diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c3cbbb7dfed..7375ca14727 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -59,12 +59,14 @@ repos:
rev: v0.2.0
hooks:
- id: ruff
- name: "Run ruff linter and apply fixes"
- args: ["--fix"]
+ name: "Run ruff import sorter"
+ args: ["--select=I", "--fix"]
+
+ - id: ruff
+ name: "Run ruff linter"
- id: ruff-format
- name: "Format Python code"
-
+ name: "Run ruff formatter"
ci:
autoupdate_schedule: weekly
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 411b90f84ee..ff44723afee 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
// hide fields if group
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
- // disable fields
- frm.toggle_enable(["is_group", "company"], false);
+ frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
if (cint(frm.doc.is_group) == 0) {
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index 78f73efff11..0c9232015d9 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -55,8 +55,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Account Number",
- "read_only": 1
+ "label": "Account Number"
},
{
"default": "0",
@@ -72,7 +71,6 @@
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
- "read_only": 1,
"remember_last_selected_value": 1,
"reqd": 1
},
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index 3b939aa3920..49d5396c0f3 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
"General Ledger",
"Balance Sheet",
"Profit and Loss Statement",
- "Cash Flow Statement",
+ "Cash Flow",
"Accounts Payable",
"Accounts Receivable",
]) {
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
index b6673795bea..d599ea65ea6 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
@@ -1525,7 +1525,8 @@
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
"Clients cr\u00e9diteurs": {
"Clients - Avances et acomptes re\u00e7us sur commandes": {
- "account_number": "4191"
+ "account_number": "4191",
+ "account_type": "Income Account"
},
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
"account_number": "4196"
@@ -3141,4 +3142,4 @@
"account_number": "7"
}
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
index 5b9a52e8f8b..4f59085db0a 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
@@ -3,4 +3,23 @@
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {},
+ enable_immutable_ledger: function (frm) {
+ if (!frm.doc.enable_immutable_ledger) {
+ return;
+ }
+
+ let msg = __("Enabling this will change the way how cancelled transactions are handled.");
+ msg += " ";
+ msg += __("Please enable only if the understand the effects of enabling this.");
+ msg += "
";
+ msg += "Do you still want to enable immutable ledger?";
+
+ frappe.confirm(
+ msg,
+ () => {},
+ () => {
+ frm.set_value("enable_immutable_ledger", 0);
+ }
+ );
+ },
});
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 0e238e08f62..9982b4f9d88 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -12,6 +12,7 @@
"unlink_advance_payment_on_cancelation_of_order",
"column_break_13",
"delete_linked_ledger_entries",
+ "enable_immutable_ledger",
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
@@ -105,7 +106,7 @@
},
{
"default": "0",
- "description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
+ "description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
@@ -454,6 +455,13 @@
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
+ },
+ {
+ "default": "0",
+ "description": "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well",
+ "fieldname": "enable_immutable_ledger",
+ "fieldtype": "Check",
+ "label": "Enable Immutable Ledger"
}
],
"icon": "icon-cog",
@@ -461,7 +469,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-01-30 14:04:26.553554",
+ "modified": "2024-03-15 12:11:36.085158",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -490,4 +498,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 07d1a65265b..34f0f24047b 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -39,6 +39,7 @@ class AccountsSettings(Document):
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
enable_common_party_accounting: DF.Check
enable_fuzzy_matching: DF.Check
+ enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index a1de91faad2..b15745d834c 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
);
frm.add_custom_button(__("Auto Reconcile"), function () {
+ if (!frm.doc.bank_account) {
+ frappe.msgprint(__("Please select Bank Account"));
+ return;
+ }
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
args: {
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 05787665a76..42b1a54dea6 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -495,12 +495,12 @@ def check_matching(
bank_account,
company,
transaction,
- document_types,
- from_date,
- to_date,
- filter_by_reference_date,
- from_reference_date,
- to_reference_date,
+ document_types=None,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
):
exact_match = True if "exact_match" in document_types else False
@@ -540,14 +540,14 @@ def get_queries(
bank_account,
company,
transaction,
- document_types,
- from_date,
- to_date,
- filter_by_reference_date,
- from_reference_date,
- to_reference_date,
- exact_match,
- common_filters,
+ document_types=None,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
+ exact_match=None,
+ common_filters=None,
):
# get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
@@ -580,15 +580,15 @@ def get_matching_queries(
bank_account,
company,
transaction,
- document_types,
- exact_match,
- account_from_to,
- from_date,
- to_date,
- filter_by_reference_date,
- from_reference_date,
- to_reference_date,
- common_filters,
+ document_types=None,
+ exact_match=None,
+ account_from_to=None,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
+ common_filters=None,
):
queries = []
currency = get_account_currency(bank_account)
diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
index 75f02e659f4..a1271b9c32a 100644
--- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
+++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
+ if "bank_ac_no" in or_filters:
+ or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
+
if party_result:
result = (
party,
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
index 784ad27dd47..34b2ec68743 100644
--- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_days, format_date, getdate
+from frappe.utils import add_days, flt, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError):
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
self.validate_child_cost_centers()
def validate_total_allocation_percentage(self):
- total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
+ total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 2719c83ba35..ad58761e1bf 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -141,7 +141,19 @@ class Dunning(AccountsController):
def on_cancel(self):
super().on_cancel()
- self.ignore_linked_doctypes = ["GL Entry"]
+ self.ignore_linked_doctypes = [
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Repost Payment Ledger",
+ "Repost Payment Ledger Items",
+ "Repost Accounting Ledger",
+ "Repost Accounting Ledger Items",
+ "Unreconcile Payment",
+ "Unreconcile Payment Entries",
+ "Payment Ledger Entry",
+ "Serial and Batch Bundle",
+ ]
def resolve_dunning(doc, state):
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
index fb997847448..66db37fe13b 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
@@ -82,7 +82,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2024-01-30 12:35:38.645968",
+ "modified": "2024-05-27 17:29:55.560840",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
@@ -127,6 +127,10 @@
{
"read": 1,
"role": "Stock Manager"
+ },
+ {
+ "read": 1,
+ "role": "Auditor"
}
],
"show_name_in_global_search": 1,
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 4de20a17389..3ec47ff022a 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -454,7 +454,7 @@ class JournalEntry(AccountsController):
self.voucher_type == "Depreciation Entry"
and d.reference_type == "Asset"
and d.reference_name
- and d.account_type == "Depreciation"
+ and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_doc("Asset", d.reference_name)
@@ -1031,6 +1031,17 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
+
+ company_currency = erpnext.get_company_currency(self.company)
+ if self.multi_currency:
+ for row in self.get("accounts"):
+ if row.account_currency != company_currency:
+ self.currency = row.account_currency
+ self.conversion_rate = row.exchange_rate
+ break
+ else:
+ self.currency = company_currency
+
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index d6ba193aa0e..d420bcca342 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -20,6 +20,7 @@
"party",
"party_name",
"book_advance_payments_in_separate_party_account",
+ "reconcile_on_advance_payment_date",
"column_break_11",
"bank_account",
"party_bank_account",
@@ -88,6 +89,7 @@
"custom_remarks",
"remarks",
"base_in_words",
+ "is_opening",
"column_break_16",
"letter_head",
"print_heading",
@@ -750,6 +752,7 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -765,6 +768,26 @@
"label": "In Words",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "company.reconcile_on_advance_payment_date",
+ "fieldname": "reconcile_on_advance_payment_date",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Reconcile on Advance Payment Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "default": "No",
+ "depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
+ "fieldname": "is_opening",
+ "fieldtype": "Select",
+ "label": "Is Opening",
+ "options": "No\nYes",
+ "print_hide": 1,
+ "search_index": 1
}
],
"index_web_pages_for_search": 1,
@@ -778,7 +801,7 @@
"table_fieldname": "payment_entries"
}
],
- "modified": "2024-04-11 11:25:07.366347",
+ "modified": "2024-05-31 17:07:06.197249",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 9f98649248d..69075e5cf6c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -116,11 +116,13 @@ class PaymentEntry(AccountsController):
self.book_advance_payments_in_separate_party_account = False
if self.party_type not in ("Customer", "Supplier"):
+ self.is_opening = "No"
return
if not frappe.db.get_value(
"Company", self.company, "book_advance_payments_in_separate_party_account"
):
+ self.is_opening = "No"
return
# Important to set this flag for the gl building logic to work properly
@@ -132,6 +134,7 @@ class PaymentEntry(AccountsController):
if (account_type == "Payable" and self.party_type == "Customer") or (
account_type == "Receivable" and self.party_type == "Supplier"
):
+ self.is_opening = "No"
return
if self.references:
@@ -141,6 +144,7 @@ class PaymentEntry(AccountsController):
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
if reference_types - allowed_types:
self.book_advance_payments_in_separate_party_account = False
+ self.is_opening = "No"
return
liability_account = get_party_account(
@@ -1249,13 +1253,16 @@ class PaymentEntry(AccountsController):
"voucher_detail_no": invoice.name,
}
- date_field = "posting_date"
- if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
- date_field = "transaction_date"
- posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
-
- if getdate(posting_date) < getdate(self.posting_date):
+ if self.reconcile_on_advance_payment_date:
posting_date = self.posting_date
+ else:
+ date_field = "posting_date"
+ if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
+ date_field = "transaction_date"
+ posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
+
+ if getdate(posting_date) < getdate(self.posting_date):
+ posting_date = self.posting_date
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
args_dict["account"] = account
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index d22350d20fc..4631f8905e6 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
self.check_gl_entries()
self.check_pl_entries()
+ def test_opening_flag_for_advance_as_liability(self):
+ company = "_Test Company"
+
+ advance_account = create_account(
+ parent_account="Current Assets - _TC",
+ account_name="Advances Received",
+ company=company,
+ account_type="Receivable",
+ )
+
+ # Enable Advance in separate party account
+ frappe.db.set_value(
+ "Company",
+ company,
+ {
+ "book_advance_payments_in_separate_party_account": 1,
+ "default_advance_received_account": advance_account,
+ },
+ )
+ # Advance Payment
+ adv = create_payment_entry(
+ party_type="Customer",
+ party="_Test Customer",
+ payment_type="Receive",
+ paid_from="Debtors - _TC",
+ paid_to="_Test Cash - _TC",
+ )
+ adv.is_opening = "Yes"
+ adv.save() # use save() to trigger set_liability_account()
+ adv.submit()
+
+ gl_with_opening_set = frappe.db.get_all(
+ "GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
+ )
+ # 'Is Opening' can be 'Yes' for Advances in separate party account
+ self.assertNotEqual(gl_with_opening_set, [])
+
+ # Disable Advance in separate party account
+ frappe.db.set_value(
+ "Company",
+ company,
+ {
+ "book_advance_payments_in_separate_party_account": 0,
+ "default_advance_received_account": None,
+ },
+ )
+ payment = create_payment_entry(
+ party_type="Customer",
+ party="_Test Customer",
+ payment_type="Receive",
+ paid_from="Debtors - _TC",
+ paid_to="_Test Cash - _TC",
+ )
+ payment.is_opening = "Yes"
+ payment.save()
+ payment.submit()
+ gl_with_opening_set = frappe.db.get_all(
+ "GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
+ )
+ # 'Is Opening' should always be 'No' for normal advance payments
+ self.assertEqual(gl_with_opening_set, [])
+
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 0a3a0678084..53f69a47e75 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
]
self.assertEqual(pl_entries, expected_ple)
+ def test_advance_payment_reconciliation_date(self):
+ frappe.db.set_value(
+ "Company",
+ self.company,
+ {
+ "book_advance_payments_in_separate_party_account": 1,
+ "default_advance_paid_account": self.advance_payable_account,
+ "reconcile_on_advance_payment_date": 1,
+ },
+ )
+
+ self.supplier = "_Test Supplier"
+ amount = 1500
+
+ pe = self.create_payment_entry(amount=amount)
+ pe.posting_date = add_days(nowdate(), -1)
+ pe.party_type = "Supplier"
+ pe.party = self.supplier
+ pe.payment_type = "Pay"
+ pe.paid_from = self.cash
+ pe.paid_to = self.advance_payable_account
+ pe.save().submit()
+
+ pi = self.create_purchase_invoice(qty=10, rate=100)
+ self.assertNotEqual(pe.posting_date, pi.posting_date)
+
+ pr = self.create_payment_reconciliation(party_is_customer=False)
+ pr.default_advance_account = self.advance_payable_account
+ pr.from_payment_date = pe.posting_date
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [invoice.as_dict() for invoice in pr.invoices]
+ payments = [payment.as_dict() for payment in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+
+ # Assert Ledger Entries
+ gl_entries = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
+ )
+ self.assertEqual(len(gl_entries), 4)
+ pl_entries = frappe.db.get_all(
+ "Payment Ledger Entry",
+ filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
+ )
+ self.assertEqual(len(pl_entries), 3)
+
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index b158edaed5f..9faf8693a8a 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
- _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
+ _("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 3350a160cea..34a31d52dd0 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
+ self.make_bundle_for_sales_purchase_return()
self.submit_serial_batch_bundle()
if self.coupon_code:
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 4816cccb285..1fca0495098 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
pos.insert()
pos.submit()
+ pos.reload()
pos_return1 = make_sales_return(pos.name)
# partial return 1
pos_return1.get("items")[0].qty = -1
+ pos_return1.submit()
+ pos_return1.reload()
bundle_id = frappe.get_doc(
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
)
- bundle_id.remove(bundle_id.entries[1])
- bundle_id.save()
-
bundle_id.load_from_db()
serial_no = bundle_id.entries[0].serial_no
self.assertEqual(serial_no, serial_nos[0])
- pos_return1.insert()
- pos_return1.submit()
-
# partial return 2
pos_return2 = make_sales_return(pos.name)
+ pos_return2.submit()
+
self.assertEqual(pos_return2.get("items")[0].qty, -1)
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(serial_no, serial_nos[1])
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 7501df02c3f..e8f94b880e2 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
- _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
+ _("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if isinstance(error_message, list):
- error_message = frappe.json.dumps(error_message)
+ error_message = json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index e8e80449292..6f191c106c9 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -74,15 +74,21 @@
"discount_amount",
"discount_percentage",
"for_price_list",
- "section_break_13",
- "threshold_percentage",
- "priority",
+ "dynamic_condition_tab",
"condition",
- "column_break_66",
+ "section_break_13",
"apply_multiple_pricing_rules",
"apply_discount_on_rate",
+ "column_break_66",
+ "threshold_percentage",
+ "validate_pricing_rule_section",
"validate_applied_rule",
+ "column_break_texp",
"rule_description",
+ "priority_section",
+ "has_priority",
+ "column_break_sayg",
+ "priority",
"help_section",
"pricing_rule_help",
"reference_section",
@@ -477,7 +483,7 @@
{
"collapsible": 1,
"fieldname": "section_break_13",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Advanced Settings"
},
{
@@ -487,6 +493,7 @@
"label": "Threshold for Suggestion (In Percentage)"
},
{
+ "depends_on": "has_priority",
"description": "Higher the number, higher the priority",
"fieldname": "priority",
"fieldtype": "Select",
@@ -513,6 +520,7 @@
{
"default": "0",
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
+ "description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
"fieldname": "validate_applied_rule",
"fieldtype": "Check",
"label": "Validate Applied Rule"
@@ -525,7 +533,8 @@
},
{
"fieldname": "help_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
+ "label": "Help Article",
"options": "Simple"
},
{
@@ -603,12 +612,42 @@
"fieldname": "apply_recursion_over",
"fieldtype": "Float",
"label": "Apply Recursion Over (As Per Transaction UOM)"
+ },
+ {
+ "fieldname": "priority_section",
+ "fieldtype": "Section Break",
+ "label": "Priority"
+ },
+ {
+ "fieldname": "dynamic_condition_tab",
+ "fieldtype": "Tab Break",
+ "label": "Dynamic Condition"
+ },
+ {
+ "fieldname": "validate_pricing_rule_section",
+ "fieldtype": "Section Break",
+ "label": "Validate Pricing Rule"
+ },
+ {
+ "fieldname": "column_break_texp",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_sayg",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Enable this checkbox even if you want to set the zero priority",
+ "fieldname": "has_priority",
+ "fieldtype": "Check",
+ "label": "Has Priority"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2023-02-14 04:53:34.887358",
+ "modified": "2024-05-17 13:16:34.496704",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index ee9a2137d55..420fd3bee9d 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -27,9 +27,7 @@ class PricingRule(Document):
from frappe.types import DF
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
- from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
- PricingRuleItemCode,
- )
+ from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
PricingRuleItemGroup,
)
@@ -67,6 +65,7 @@ class PricingRule(Document):
free_item_rate: DF.Currency
free_item_uom: DF.Link | None
free_qty: DF.Float
+ has_priority: DF.Check
is_cumulative: DF.Check
is_recursive: DF.Check
item_groups: DF.Table[PricingRuleItemGroup]
@@ -156,6 +155,12 @@ class PricingRule(Document):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
def validate_mandatory(self):
+ if self.has_priority and not self.priority:
+ throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
+
+ if self.priority and not self.has_priority:
+ self.has_priority = 1
+
for apply_on, field in apply_on_dict.items():
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 5df689e5a2b..7f9c55ff24f 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
+ def test_pricing_rule_for_transaction_with_condition(self):
+ make_item("PR Transaction Condition")
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
+ make_pricing_rule(
+ selling=1,
+ min_qty=0,
+ price_or_product_discount="Product",
+ apply_on="Transaction",
+ free_item="PR Transaction Condition",
+ free_qty=1,
+ free_item_rate=10,
+ condition="customer=='_Test Customer 1'",
+ )
+
+ si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
+ self.assertEqual(len(si.items), 2)
+ self.assertEqual(si.items[1].rate, 10)
+
+ si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
+ self.assertEqual(len(si1.items), 1)
+
+ for doc in [si, si1]:
+ doc.delete()
+
def test_remove_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
@@ -1157,6 +1181,62 @@ class TestPricingRule(unittest.TestCase):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
+ def test_priority_of_multiple_pricing_rules(self):
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
+
+ test_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule 1",
+ "name": "_Test Pricing Rule 1",
+ "apply_on": "Item Code",
+ "currency": "USD",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
+ "selling": 1,
+ "price_or_product_discount": "Price",
+ "rate_or_discount": "Discount Percentage",
+ "discount_percentage": 10,
+ "has_priority": 1,
+ "priority": 1,
+ "company": "_Test Company",
+ }
+
+ frappe.get_doc(test_record.copy()).insert()
+
+ test_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule 2",
+ "name": "_Test Pricing Rule 2",
+ "apply_on": "Item Code",
+ "currency": "USD",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
+ "selling": 1,
+ "price_or_product_discount": "Price",
+ "rate_or_discount": "Discount Percentage",
+ "discount_percentage": 20,
+ "has_priority": 1,
+ "priority": 3,
+ "company": "_Test Company",
+ }
+
+ frappe.get_doc(test_record.copy()).insert()
+
+ so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
+ self.assertEqual(so.items[0].discount_percentage, 20)
+ self.assertEqual(so.items[0].rate, 800)
+
+ frappe.delete_doc_if_exists("Sales Order", so.name)
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
+
test_dependencies = ["Campaign"]
@@ -1185,6 +1265,7 @@ def make_pricing_rule(**args):
"priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
+ "has_priority": args.has_priority or 0,
}
)
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 44f7f33a319..60c9e26aabe 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -33,6 +33,9 @@ def get_pricing_rules(args, doc=None):
for apply_on in ["Item Code", "Item Group", "Brand"]:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
+ if pricing_rules and pricing_rules[0].has_priority:
+ continue
+
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
break
@@ -561,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
if pricing_rules:
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
+ pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules:
remove_free_item(doc)
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
index d4d66a51c34..5048fc5e25e 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import json
+
import frappe
from frappe import _, qb
from frappe.model.document import Document
@@ -504,7 +506,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if isinstance(for_filter, str):
- for_filter = frappe.json.loads(for_filter)
+ for_filter = json.loads(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index c73c13eb118..e0ec144e314 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
ageing_filters = frappe._dict(
{
"company": doc.company,
- "report_date": doc.to_date,
+ "report_date": doc.posting_date,
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
index 647600a9fea..bf8de073853 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
@@ -340,10 +340,11 @@
- | 30 Days |
- 60 Days |
- 90 Days |
- 120 Days |
+ 0 - 30 Days |
+ 30 - 60 Days |
+ 60 - 90 Days |
+ 90 - 120 Days |
+ Above 120 Days |
@@ -352,6 +353,7 @@
{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }} |
{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }} |
{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }} |
+ {{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }} |
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 957611f7858..c0a43395216 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -485,10 +485,12 @@ function hide_fields(doc) {
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
- cur_frm.fields_dict["items"].grid.set_column_disp(
- item_fields_stock,
- cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
- );
+ if (cur_frm.fields_dict["items"]) {
+ cur_frm.fields_dict["items"].grid.set_column_disp(
+ item_fields_stock,
+ cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
+ );
+ }
cur_frm.refresh_fields();
}
@@ -675,7 +677,7 @@ frappe.ui.form.on("Purchase Invoice", {
if (frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
- if (!frm.doc.__onload.supplier_tds) {
+ if (!frm.doc.__onload.enable_apply_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 272a180ca8d..fba60cef632 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, throw
+from frappe import _, qb, throw
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
@@ -347,6 +347,22 @@ class PurchaseInvoice(BuyingController):
self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
+ # If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
+ if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
+ po = qb.DocType("Purchase Order")
+ po_with_tds = (
+ qb.from_(po)
+ .select(po.name)
+ .where(
+ po.docstatus.eq(1)
+ & (po.name.isin(purchase_orders))
+ & (po.apply_tds.eq(1))
+ & (po.tax_withholding_category.notnull())
+ )
+ .run()
+ )
+ self.set_onload("enable_apply_tds", True if po_with_tds else False)
+
super().set_missing_values(for_validate)
def validate_credit_to_acc(self):
@@ -448,7 +464,7 @@ class PurchaseInvoice(BuyingController):
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
stock_items = self.get_stock_items()
- asset_received_but_not_billed = None
+ self.asset_received_but_not_billed = None
if self.update_stock:
self.validate_item_code()
@@ -531,26 +547,45 @@ class PurchaseInvoice(BuyingController):
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
- elif item.is_fixed_asset and item.pr_detail:
- if not asset_received_but_not_billed:
- asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
- item.expense_account = asset_received_but_not_billed
elif item.is_fixed_asset:
- account_type = (
- "capital_work_in_progress_account"
- if is_cwip_accounting_enabled(item.asset_category)
- else "fixed_asset_account"
- )
- asset_category_account = get_asset_category_account(
- account_type, item=item.item_code, company=self.company
- )
- if not asset_category_account:
- form_link = get_link_to_form("Asset Category", item.asset_category)
- throw(
- _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
- title=_("Missing Account"),
+ account = None
+ if item.pr_detail:
+ if not self.asset_received_but_not_billed:
+ self.asset_received_but_not_billed = self.get_company_default(
+ "asset_received_but_not_billed"
+ )
+
+ # check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
+ arbnb_booked_in_pr = frappe.db.get_value(
+ "GL Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": item.purchase_receipt,
+ "account": self.asset_received_but_not_billed,
+ },
+ "name",
)
- item.expense_account = asset_category_account
+ if arbnb_booked_in_pr:
+ account = self.asset_received_but_not_billed
+
+ if not account:
+ account_type = (
+ "capital_work_in_progress_account"
+ if is_cwip_accounting_enabled(item.asset_category)
+ else "fixed_asset_account"
+ )
+ account = get_asset_category_account(
+ account_type, item=item.item_code, company=self.company
+ )
+ if not account:
+ form_link = get_link_to_form("Asset Category", item.asset_category)
+ throw(
+ _("Please set Fixed Asset Account in {} against {}.").format(
+ form_link, self.company
+ ),
+ title=_("Missing Account"),
+ )
+ item.expense_account = account
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -648,6 +683,19 @@ class PurchaseInvoice(BuyingController):
where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
}
)
+ self.status_updater.append(
+ {
+ "source_dt": "Purchase Invoice Item",
+ "target_dt": "Material Request Item",
+ "join_field": "material_request_item",
+ "target_field": "received_qty",
+ "target_parent_dt": "Material Request",
+ "target_parent_field": "per_received",
+ "target_ref_field": "stock_qty",
+ "source_field": "stock_qty",
+ "percent_join_field": "material_request",
+ }
+ )
if cint(self.is_return):
self.status_updater.append(
{
@@ -707,6 +755,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
+ self.make_bundle_for_sales_purchase_return()
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
@@ -1035,10 +1084,10 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
- provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
+ provisional_account, pr_qty, pr_base_rate, pr_rate = frappe.get_cached_value(
"Purchase Receipt Item",
item.pr_detail,
- ["provisional_expense_account", "qty", "base_rate"],
+ ["provisional_expense_account", "qty", "base_rate", "rate"],
)
provisional_account = provisional_account or self.get_company_default(
"default_provisional_account"
@@ -1072,7 +1121,10 @@ class PurchaseInvoice(BuyingController):
self.posting_date,
provisional_account,
reverse=1,
- item_amount=(min(item.qty, pr_qty) * pr_base_rate),
+ item_amount=(
+ (min(item.qty, pr_qty) * pr_rate)
+ * purchase_receipt_doc.get("conversion_rate")
+ ),
)
if not self.is_internal_transfer():
@@ -1182,7 +1234,7 @@ class PurchaseInvoice(BuyingController):
asset.name,
{
"gross_purchase_amount": purchase_amount,
- "purchase_receipt_amount": purchase_amount,
+ "purchase_amount": purchase_amount,
},
)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 3ea88f195d8..3c3b081fa2f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -17,6 +17,8 @@ from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+from erpnext.stock.doctype.material_request.test_material_request import make_material_request
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
@@ -72,6 +74,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# teardown
pi.delete()
+ def test_update_received_qty_in_material_request(self):
+ from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
+
+ """
+ Test if the received_qty in Material Request is updated correctly when
+ a Purchase Invoice with update_stock=True is submitted.
+ """
+ mr = make_material_request(item_code="_Test Item", qty=10)
+ mr.save()
+ mr.submit()
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.save()
+ po.submit()
+
+ # Create a Purchase Invoice with update_stock=True
+ pi = make_purchase_invoice(po.name)
+ pi.update_stock = True
+ pi.insert()
+ pi.submit()
+
+ # Check if the received quantity is updated in Material Request
+ mr.reload()
+ self.assertEqual(mr.items[0].received_qty, 10)
+
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
pi = frappe.copy_doc(test_records[0])
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 66df76a3af0..1d7b0c2f461 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -105,6 +105,8 @@
"purchase_receipt",
"pr_detail",
"sales_invoice_item",
+ "material_request",
+ "material_request_item",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -934,12 +936,34 @@
{
"fieldname": "column_break_vbbb",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "material_request",
+ "fieldtype": "Link",
+ "label": "Material Request",
+ "no_copy": 1,
+ "options": "Material Request",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "material_request_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Material Request Item",
+ "no_copy": 1,
+ "oldfieldname": "pr_detail",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-19 19:09:47.210965",
+ "modified": "2024-06-14 11:57:07.171700",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -949,4 +973,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
index ccbc34749d7..baeece4815c 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
@@ -52,6 +52,8 @@ class PurchaseInvoiceItem(Document):
manufacturer_part_no: DF.Data | None
margin_rate_or_amount: DF.Float
margin_type: DF.Literal["", "Percentage", "Amount"]
+ material_request: DF.Link | None
+ material_request_item: DF.Data | None
net_amount: DF.Currency
net_rate: DF.Currency
page_break: DF.Check
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
index 5b7cd2b0b20..0d74b2150f7 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -1,7 +1,5 @@
{
"actions": [],
- "allow_rename": 1,
- "autoname": "format:ACC-REPOST-{#####}",
"creation": "2023-07-04 13:07:32.923675",
"default_view": "List",
"doctype": "DocType",
@@ -55,14 +53,15 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-09-26 14:21:27.362567",
+ "modified": "2024-06-03 17:30:37.012593",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
- "naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -71,7 +70,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
index 8aa0a840c7e..dd314ca6601 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
@@ -17,7 +17,7 @@
"in_create": 1,
"issingle": 1,
"links": [],
- "modified": "2023-11-07 14:24:13.321522",
+ "modified": "2024-06-06 13:56:37.908879",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
@@ -30,13 +30,17 @@
"print": 1,
"read": 1,
"role": "Administrator",
+ "select": 1,
"share": 1,
"write": 1
},
{
+ "create": 1,
+ "delete": 1,
"read": 1,
"role": "System Manager",
- "select": 1
+ "select": 1,
+ "write": 1
}
],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
index ed8d395a0ec..2b4efead7f3 100644
--- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_rename": 1,
"creation": "2022-10-19 21:59:33.553852",
"doctype": "DocType",
"editable_grid": 1,
@@ -99,13 +98,15 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-09-26 14:21:35.719727",
+ "modified": "2024-06-03 17:31:04.472279",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
"owner": "Administrator",
"permissions": [
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -114,7 +115,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
},
{
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 1b36bf50f86..90b6e768092 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -787,6 +787,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
+ "no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2187,7 +2188,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2024-05-08 18:02:28.549041",
+ "modified": "2024-06-07 16:49:32.458402",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 63f13ae251f..cb0803db932 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
if not self.get(table_name):
continue
+ self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger()
@@ -2677,6 +2678,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
+ # update outstanding
+ if source.payment_schedule and len(source.payment_schedule) == 1:
+ target.overdue_payments[0].outstanding = source.get("outstanding_amount")
+
target.validate()
return get_mapped_doc(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ee8658a9414..5cffe11e995 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
+import json
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
map_docs(
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
- source_names=frappe.json.dumps([dn1.name, dn2.name]),
+ source_names=json.dumps([dn1.name, dn2.name]),
target_doc=si,
- args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
+ args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
)
si.save().submit()
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index e7536e9bb94..932bc8e49d4 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -870,7 +870,8 @@
"label": "Purchase Order",
"options": "Purchase Order",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "column_break_92",
@@ -926,7 +927,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-02-25 15:56:44.828634",
+ "modified": "2024-05-23 16:36:18.970862",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -936,4 +937,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
index edbd78db611..7612294a85c 100644
--- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
+++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import json
+
import frappe
from frappe import _, qb
from frappe.model.document import Document
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
if selections:
- selections = frappe.json.loads(selections)
+ selections = json.loads(selections)
# assuming each row is a unique voucher
for row in selections:
unrecon = frappe.new_doc("Unreconcile Payment")
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 0ff9e973e59..3f01dee888d 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -577,6 +577,8 @@ def make_reverse_gl_entries(
and make reverse gl entries by swapping debit and credit
"""
+ immutable_ledger_enabled = is_immutable_ledger_enabled()
+
if not gl_entries:
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
@@ -608,7 +610,6 @@ def make_reverse_gl_entries(
for x in gl_entries:
query = (
frappe.qb.update(gle)
- .set(gle.is_cancelled, True)
.set(gle.modified, now())
.set(gle.modified_by, frappe.session.user)
.where(
@@ -623,9 +624,14 @@ def make_reverse_gl_entries(
& (gle.voucher_detail_no == x.voucher_detail_no)
)
)
+
+ if not immutable_ledger_enabled:
+ query = query.set(gle.is_cancelled, True)
+
query.run()
else:
- set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
+ if not immutable_ledger_enabled:
+ set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
for entry in gl_entries:
new_gle = copy.deepcopy(entry)
@@ -644,6 +650,10 @@ def make_reverse_gl_entries(
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
new_gle["is_cancelled"] = 1
+ if immutable_ledger_enabled:
+ new_gle["is_cancelled"] = 0
+ new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
+
if new_gle["debit"] or new_gle["credit"]:
make_entry(new_gle, adv_adj, "Yes")
@@ -736,3 +746,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
),
InvalidAccountDimensionError,
)
+
+
+def is_immutable_ledger_enabled():
+ return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py
index 6f9f7ebcb8d..f385c71a9f4 100644
--- a/erpnext/accounts/report/account_balance/account_balance.py
+++ b/erpnext/accounts/report/account_balance/account_balance.py
@@ -49,7 +49,6 @@ def get_conditions(filters):
if filters.account_type:
conditions["account_type"] = filters.account_type
- return conditions
if filters.company:
conditions["company"] = filters.company
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 2bd493cd4d0..64dba0183dc 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -1028,20 +1028,6 @@ class ReceivablePayableReport:
fieldtype="Link",
options="Contact",
)
- if self.filters.party_type == "Customer":
- self.add_column(
- _("Customer Name"),
- fieldname="customer_name",
- fieldtype="Link",
- options="Customer",
- )
- elif self.filters.party_type == "Supplier":
- self.add_column(
- _("Supplier Name"),
- fieldname="supplier_name",
- fieldtype="Link",
- options="Supplier",
- )
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
index d285f28d8e3..e1545bdcd87 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder import DocType
from frappe.utils import cstr, flt
@@ -75,11 +76,24 @@ def get_data(filters):
asset_data = assets_details.get(d.against_voucher)
if asset_data:
if not asset_data.get("accumulated_depreciation_amount"):
- asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
- "opening_accumulated_depreciation"
- )
+ AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
+ DepreciationSchedule = DocType("Depreciation Schedule")
+ query = (
+ frappe.qb.from_(DepreciationSchedule)
+ .join(AssetDepreciationSchedule)
+ .on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
+ .select(DepreciationSchedule.accumulated_depreciation_amount)
+ .where(
+ (AssetDepreciationSchedule.asset == d.against_voucher)
+ & (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
+ & (DepreciationSchedule.schedule_date == d.posting_date)
+ )
+ ).run(as_dict=True)
+ asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
+
else:
asset_data.accumulated_depreciation_amount += d.debit
+ asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
row = frappe._dict(asset_data)
row.update(
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index 4fa485f54b3..f9a008ade7f 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
+ # nosemgrep
return frappe.db.sql(
f"""
- SELECT asset_category,
- ifnull(sum(case when purchase_date < %(from_date)s then
- case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
- gross_purchase_amount
+ SELECT a.asset_category,
+ ifnull(sum(case when a.purchase_date < %(from_date)s then
+ case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
+ a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
- ifnull(sum(case when purchase_date >= %(from_date)s then
- gross_purchase_amount
+ ifnull(sum(case when a.purchase_date >= %(from_date)s then
+ a.gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
- ifnull(sum(case when ifnull(disposal_date, 0) != 0
- and disposal_date >= %(from_date)s
- and disposal_date <= %(to_date)s then
- case when status = "Sold" then
- gross_purchase_amount
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
+ and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s then
+ case when a.status = "Sold" then
+ a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
- ifnull(sum(case when ifnull(disposal_date, 0) != 0
- and disposal_date >= %(from_date)s
- and disposal_date <= %(to_date)s then
- case when status = "Scrapped" then
- gross_purchase_amount
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
+ and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s then
+ case when a.status = "Scrapped" then
+ a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
- from `tabAsset`
+ from `tabAsset` a
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
- group by asset_category
+ and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
+ group by a.asset_category
""",
{
"to_date": filters.to_date,
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 02de6c3aee1..e884deb2a17 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -219,7 +219,8 @@ def get_conditions(filters):
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
- conditions.append("account in %(account)s")
+ if filters.account:
+ conditions.append("account in %(account)s")
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
else:
frappe.throw(_("Account: {0} does not exist").format(d))
- return list(set(all_accounts))
+ return list(set(all_accounts)) if all_accounts else None
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
@@ -420,6 +421,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
if filters.get("show_net_values_in_party_account"):
account_type_map = get_account_type_map(filters.get("company"))
+ immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
+
def update_value_in_dict(data, key, gle):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -481,12 +484,17 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [
+ gle.get("posting_date"),
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
+
+ if immutable_ledger:
+ keylist.append(gle.get("creation"))
+
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index ebdea0602d1..1cd9b872a9b 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -5,14 +5,15 @@
import frappe
from frappe import _
from frappe.utils import flt
+from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
add_sub_total_row,
add_total_row,
+ apply_group_by_conditions,
get_grand_total,
get_group_by_and_display_fields,
- get_group_by_conditions,
get_tax_accounts,
)
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
@@ -29,7 +30,7 @@ def _execute(filters=None, additional_table_columns=None):
company_currency = erpnext.get_company_currency(filters.company)
- item_list = get_items(filters, get_query_columns(additional_table_columns))
+ item_list = get_items(filters, additional_table_columns)
aii_account_map = get_aii_accounts()
if item_list:
itemised_tax, tax_columns = get_tax_accounts(
@@ -287,59 +288,90 @@ def get_columns(additional_table_columns, filters):
return columns
-def get_conditions(filters):
- conditions = ""
+def apply_conditions(query, pi, pii, filters):
+ for opts in ("company", "supplier", "mode_of_payment"):
+ if filters.get(opts):
+ query = query.where(pi[opts] == filters[opts])
- for opts in (
- ("company", " and `tabPurchase Invoice`.company=%(company)s"),
- ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
- ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
- ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
- ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
- ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
- ("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
- ):
- if filters.get(opts[0]):
- conditions += opts[1]
+ if filters.get("from_date"):
+ query = query.where(pi.posting_date >= filters.get("from_date"))
+
+ if filters.get("to_date"):
+ query = query.where(pi.posting_date <= filters.get("to_date"))
+
+ if filters.get("item_code"):
+ query = query.where(pii.item_code == filters.get("item_code"))
+
+ if filters.get("item_group"):
+ query = query.where(pii.item_group == filters.get("item_group"))
if not filters.get("group_by"):
- conditions += (
- "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
- )
+ query = query.orderby(pi.posting_date, order=Order.desc)
+ query = query.orderby(pii.item_group, order=Order.desc)
else:
- conditions += get_group_by_conditions(filters, "Purchase Invoice")
+ query = apply_group_by_conditions(filters, "Purchase Invoice")
- return conditions
+ return query
-def get_items(filters, additional_query_columns):
- conditions = get_conditions(filters)
- if additional_query_columns:
- additional_query_columns = "," + ",".join(additional_query_columns)
- return frappe.db.sql(
- f"""
- select
- `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
- `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
- `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
- `tabPurchase Invoice`.unrealized_profit_loss_account,
- `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
- `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
- `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
- `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
- `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
- `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
- `tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
- `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {additional_query_columns}
- from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
- where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
- `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
- `tabPurchase Invoice`.docstatus = 1 {conditions}
- """,
- filters,
- as_dict=1,
+def get_items(filters, additional_table_columns):
+ pi = frappe.qb.DocType("Purchase Invoice")
+ pii = frappe.qb.DocType("Purchase Invoice Item")
+ Item = frappe.qb.DocType("Item")
+ query = (
+ frappe.qb.from_(pi)
+ .join(pii)
+ .on(pi.name == pii.parent)
+ .left_join(Item)
+ .on(pii.item_code == Item.name)
+ .select(
+ pii.name,
+ pii.parent,
+ pi.posting_date,
+ pi.credit_to,
+ pi.company,
+ pi.supplier,
+ pi.remarks,
+ pi.base_net_total,
+ pi.unrealized_profit_loss_account,
+ pii.item_code,
+ pii.description,
+ pii.item_group,
+ pii.item_name.as_("pi_item_name"),
+ pii.item_group.as_("pi_item_group"),
+ Item.item_name.as_("i_item_name"),
+ Item.item_group.as_("i_item_group"),
+ pii.project,
+ pii.purchase_order,
+ pii.purchase_receipt,
+ pii.po_detail,
+ pii.expense_account,
+ pii.stock_qty,
+ pii.stock_uom,
+ pii.base_net_amount,
+ pi.supplier_name,
+ pi.mode_of_payment,
+ )
+ .where(pi.docstatus == 1)
)
+ if filters.get("supplier"):
+ query = query.where(pi.supplier == filters["supplier"])
+ if filters.get("company"):
+ query = query.where(pi.company == filters["company"])
+
+ if additional_table_columns:
+ for column in additional_table_columns:
+ if column.get("_doctype"):
+ table = frappe.qb.DocType(column.get("_doctype"))
+ query = query.select(table[column.get("fieldname")])
+ else:
+ query = query.select(pi[column.get("fieldname")])
+
+ query = apply_conditions(query, pi, pii, filters)
+
+ return query.run(as_dict=True)
+
def get_aii_accounts():
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
index 7ed2ebd89ab..16a97733393 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
@@ -41,6 +41,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
+ get_query: function () {
+ const company = frappe.query_report.get_filter_value("company");
+ return {
+ filters: { company: company },
+ };
+ },
},
{
fieldname: "brand",
@@ -48,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
fieldtype: "Link",
options: "Brand",
},
+ {
+ fieldname: "item_code",
+ label: __("Item"),
+ fieldtype: "Link",
+ options: "Item",
+ },
{
fieldname: "item_group",
label: __("Item Group"),
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index c7819731930..cf08e45c537 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -7,6 +7,7 @@ from frappe import _
from frappe.model.meta import get_field_precision
from frappe.utils import cstr, flt
from frappe.utils.xlsxutils import handle_html
+from pypika import Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
@@ -26,7 +27,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
- item_list = get_items(filters, get_query_columns(additional_table_columns), additional_conditions)
+ item_list = get_items(filters, additional_table_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -83,9 +84,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
"company": d.company,
"sales_order": d.sales_order,
"delivery_note": d.delivery_note,
- "income_account": d.unrealized_profit_loss_account
- if d.is_internal_customer == 1
- else d.income_account,
+ "income_account": get_income_account(d),
"cost_center": d.cost_center,
"stock_qty": d.stock_qty,
"stock_uom": d.stock_uom,
@@ -150,6 +149,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
return columns, data, None, None, None, skip_total_row
+def get_income_account(row):
+ if row.enable_deferred_revenue:
+ return row.deferred_revenue_account
+ elif row.is_internal_customer == 1:
+ return row.unrealized_profit_loss_account
+ else:
+ return row.income_account
+
+
def get_columns(additional_table_columns, filters):
columns = []
@@ -333,93 +341,143 @@ def get_columns(additional_table_columns, filters):
return columns
-def get_conditions(filters, additional_conditions=None):
- conditions = ""
+def apply_conditions(query, si, sii, filters, additional_conditions=None):
+ for opts in ("company", "customer"):
+ if filters.get(opts):
+ query = query.where(si[opts] == filters[opts])
- for opts in (
- ("company", " and `tabSales Invoice`.company=%(company)s"),
- ("customer", " and `tabSales Invoice`.customer = %(customer)s"),
- ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
- ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),
- ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"),
- ):
- if filters.get(opts[0]):
- conditions += opts[1]
+ if filters.get("from_date"):
+ query = query.where(si.posting_date >= filters.get("from_date"))
- if additional_conditions:
- conditions += additional_conditions
+ if filters.get("to_date"):
+ query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"):
- conditions += """ and exists(select name from `tabSales Invoice Payment`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
+ sales_invoice = frappe.db.get_all(
+ "Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
+ )
+ query = query.where(si.name.isin(sales_invoice))
if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
lft, rgt = frappe.db.get_all(
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
)[0]
- conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
+ warehouses = frappe.db.get_all("Warehouse", {"lft": (">", lft), "rgt": ("<", rgt)}, pluck="name")
+ query = query.where(sii.warehouse.isin(warehouses))
else:
- conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
+ query = query.where(sii.warehouse == filters.get("warehouse"))
if filters.get("brand"):
- conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
+ query = query.where(sii.brand == filters.get("brand"))
+
+ if filters.get("item_code"):
+ query = query.where(sii.item_code == filters.get("item_code"))
if filters.get("item_group"):
- conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s"""
+ query = query.where(sii.item_group == filters.get("item_group"))
+
+ if filters.get("income_account"):
+ query = query.where(
+ (sii.income_account == filters.get("income_account"))
+ | (sii.deferred_revenue_account == filters.get("income_account"))
+ | (si.unrealized_profit_loss_account == filters.get("income_account"))
+ )
if not filters.get("group_by"):
- conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc"
+ query = query.orderby(si.posting_date, order=Order.desc)
+ query = query.orderby(sii.item_group, order=Order.desc)
else:
- conditions += get_group_by_conditions(filters, "Sales Invoice")
+ query = apply_group_by_conditions(query, si, sii, filters)
- return conditions
+ for key, value in (additional_conditions or {}).items():
+ query = query.where(si[key] == value)
+
+ return query
-def get_group_by_conditions(filters, doctype):
+def apply_group_by_conditions(query, si, ii, filters):
if filters.get("group_by") == "Invoice":
- return f"ORDER BY `tab{doctype} Item`.parent desc"
+ query = query.orderby(ii.parent, order=Order.desc)
elif filters.get("group_by") == "Item":
- return f"ORDER BY `tab{doctype} Item`.`item_code`"
+ query = query.orderby(ii.item_code)
elif filters.get("group_by") == "Item Group":
- return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
+ query = query.orderby(ii.item_group)
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
- return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
+ query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
+
+ return query
def get_items(filters, additional_query_columns, additional_conditions=None):
- conditions = get_conditions(filters, additional_conditions)
+ si = frappe.qb.DocType("Sales Invoice")
+ sii = frappe.qb.DocType("Sales Invoice Item")
+ item = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(si)
+ .join(sii)
+ .on(si.name == sii.parent)
+ .left_join(item)
+ .on(sii.item_code == item.name)
+ .select(
+ sii.name,
+ sii.parent,
+ si.posting_date,
+ si.debit_to,
+ si.unrealized_profit_loss_account,
+ si.is_internal_customer,
+ si.customer,
+ si.remarks,
+ si.territory,
+ si.company,
+ si.base_net_total,
+ sii.project,
+ sii.item_code,
+ sii.description,
+ sii.item_name,
+ sii.item_group,
+ sii.item_name.as_("si_item_name"),
+ sii.item_group.as_("si_item_group"),
+ item.item_name.as_("i_item_name"),
+ item.item_group.as_("i_item_group"),
+ sii.sales_order,
+ sii.delivery_note,
+ sii.income_account,
+ sii.cost_center,
+ sii.enable_deferred_revenue,
+ sii.deferred_revenue_account,
+ sii.stock_qty,
+ sii.stock_uom,
+ sii.base_net_rate,
+ sii.base_net_amount,
+ si.customer_name,
+ si.customer_group,
+ sii.so_detail,
+ si.update_stock,
+ sii.uom,
+ sii.qty,
+ )
+ .where(si.docstatus == 1)
+ )
+
if additional_query_columns:
- additional_query_columns = "," + ",".join(additional_query_columns)
- return frappe.db.sql(
- """
- select
- `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
- `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
- `tabSales Invoice`.unrealized_profit_loss_account,
- `tabSales Invoice`.is_internal_customer,
- `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
- `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
- `tabSales Invoice Item`.project,
- `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
- `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
- `tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
- `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
- `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
- `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
- `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
- `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
- `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
- `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {}
- from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
- where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
- `tabItem`.name = `tabSales Invoice Item`.`item_code` and
- `tabSales Invoice`.docstatus = 1 {}
- """.format(additional_query_columns, conditions),
- filters,
- as_dict=1,
- ) # nosec
+ for column in additional_query_columns:
+ if column.get("_doctype"):
+ table = frappe.qb.DocType(column.get("_doctype"))
+ query = query.select(table[column.get("fieldname")])
+ else:
+ query = query.select(si[column.get("fieldname")])
+
+ if filters.get("customer"):
+ query = query.where(si.customer == filters["customer"])
+
+ if filters.get("customer_group"):
+ query = query.where(si.customer_group == filters["customer_group"])
+
+ query = apply_conditions(query, si, sii, filters, additional_conditions)
+
+ return query.run(as_dict=True)
def get_delivery_notes_against_sales_order(item_list):
@@ -427,16 +485,14 @@ def get_delivery_notes_against_sales_order(item_list):
so_item_rows = list(set([d.so_detail for d in item_list]))
if so_item_rows:
- delivery_notes = frappe.db.sql(
- """
- select parent, so_detail
- from `tabDelivery Note Item`
- where docstatus=1 and so_detail in (%s)
- group by so_detail, parent
- """
- % (", ".join(["%s"] * len(so_item_rows))),
- tuple(so_item_rows),
- as_dict=1,
+ dn_item = frappe.qb.DocType("Delivery Note Item")
+ delivery_notes = (
+ frappe.qb.from_(dn_item)
+ .select(dn_item.parent, dn_item.so_detail)
+ .where(dn_item.docstatus == 1)
+ .where(dn_item.so_detail.isin(so_item_rows))
+ .groupby(dn_item.so_detail, dn_item.parent)
+ .run(as_dict=True)
)
for dn in delivery_notes:
@@ -446,15 +502,16 @@ def get_delivery_notes_against_sales_order(item_list):
def get_grand_total(filters, doctype):
- return frappe.db.sql(
- f""" SELECT
- SUM(`tab{doctype}`.base_grand_total)
- FROM `tab{doctype}`
- WHERE `tab{doctype}`.docstatus = 1
- and posting_date between %s and %s
- """,
- (filters.get("from_date"), filters.get("to_date")),
- )[0][0] # nosec
+ return flt(
+ frappe.db.get_value(
+ doctype,
+ {
+ "docstatus": 1,
+ "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
+ },
+ "sum(base_grand_total)",
+ )
+ )
def get_tax_accounts(
diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
index 834eb5f519c..f3f30d38a04 100644
--- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
+++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
@@ -3,7 +3,9 @@
import frappe
-from frappe import _
+from frappe import _, qb
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Abs
from frappe.utils import flt, getdate
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -21,16 +23,12 @@ def execute(filters=None):
data = []
for d in entries:
- invoice = invoice_details.get(d.against_voucher) or frappe._dict()
-
- if d.reference_type == "Purchase Invoice":
- payment_amount = flt(d.debit) or -1 * flt(d.credit)
- else:
- payment_amount = flt(d.credit) or -1 * flt(d.debit)
+ invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
+ payment_amount = d.amount
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
- if d.against_voucher:
+ if d.against_voucher_no:
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
row = [
@@ -39,11 +37,10 @@ def execute(filters=None):
d.party_type,
d.party,
d.posting_date,
- d.against_voucher,
+ d.against_voucher_no,
invoice.posting_date,
invoice.due_date,
- d.debit,
- d.credit,
+ d.amount,
d.remarks,
d.age,
d.range1,
@@ -111,8 +108,7 @@ def get_columns(filters):
"width": 100,
},
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
- {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
- {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
+ {"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
@@ -129,51 +125,68 @@ def get_columns(filters):
def get_conditions(filters):
+ ple = qb.DocType("Payment Ledger Entry")
conditions = []
- if not filters.party_type:
- if filters.payment_type == _("Outgoing"):
- filters.party_type = "Supplier"
- else:
- filters.party_type = "Customer"
-
- if filters.party_type:
- conditions.append("party_type=%(party_type)s")
+ conditions.append(ple.delinked.eq(0))
+ if filters.payment_type == _("Outgoing"):
+ conditions.append(ple.party_type.eq("Supplier"))
+ conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
+ else:
+ conditions.append(ple.party_type.eq("Customer"))
+ conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
if filters.party:
- conditions.append("party=%(party)s")
-
- if filters.party_type:
- conditions.append("against_voucher_type=%(reference_type)s")
- filters["reference_type"] = (
- "Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
- )
+ conditions.append(ple.party.eq(filters.party))
if filters.get("from_date"):
- conditions.append("posting_date >= %(from_date)s")
+ conditions.append(ple.posting_date.gte(filters.get("from_date")))
if filters.get("to_date"):
- conditions.append("posting_date <= %(to_date)s")
+ conditions.append(ple.posting_date.lte(filters.get("to_date")))
- return "and " + " and ".join(conditions) if conditions else ""
+ if filters.get("company"):
+ conditions.append(ple.company.eq(filters.get("company")))
+
+ return conditions
def get_entries(filters):
- return frappe.db.sql(
- """select
- voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
- from `tabGL Entry`
- where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
- """.format(get_conditions(filters)),
- filters,
- as_dict=1,
+ ple = qb.DocType("Payment Ledger Entry")
+ conditions = get_conditions(filters)
+
+ query = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.party_type,
+ ple.party,
+ ple.posting_date,
+ Abs(ple.amount).as_("amount"),
+ ple.remarks,
+ ple.against_voucher_no,
+ )
+ .where(Criterion.all(conditions))
)
+ res = query.run(as_dict=True)
+ return res
def get_invoice_posting_date_map(filters):
invoice_details = {}
- dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
- for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
+ dt = (
+ qb.DocType("Sales Invoice")
+ if filters.get("payment_type") == _("Incoming")
+ else qb.DocType("Purchase Invoice")
+ )
+ res = (
+ qb.from_(dt)
+ .select(dt.name, dt.posting_date, dt.due_date)
+ .where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
+ .run(as_dict=1)
+ )
+ for t in res:
invoice_details[t.name] = t
return invoice_details
diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js
index e3f90f29982..0e84f882b51 100644
--- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js
+++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js
@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
- frappe.query_reports["Purchase Invoice Trends"] = {
- filters: erpnext.get_purchase_trends_filters(),
- };
-});
+frappe.query_reports["Purchase Invoice Trends"] = $.extend({}, erpnext.purchase_trends_filters);
diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js
index 292d827b163..bdc39f36a8e 100644
--- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js
+++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js
@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
- frappe.query_reports["Sales Invoice Trends"] = {
- filters: erpnext.get_sales_trends_filters(),
- };
-});
+frappe.query_reports["Sales Invoice Trends"] = $.extend({}, erpnext.sales_trends_filters);
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index f27569531b1..6c0bf91e3f8 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -80,6 +80,7 @@ def _execute(filters, additional_table_columns=None):
delivery_note = list(set(invoice_so_dn_map.get(inv.name, {}).get("delivery_note", [])))
cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", [])))
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
+ inv_customer_details = customer_details.get(inv.customer, {})
row = {
"voucher_type": inv.doctype,
@@ -88,9 +89,9 @@ def _execute(filters, additional_table_columns=None):
"customer": inv.customer,
"customer_name": inv.customer_name,
**get_values_for_columns(additional_table_columns, inv),
- "customer_group": customer_details.get(inv.customer).get("customer_group"),
- "territory": customer_details.get(inv.customer).get("territory"),
- "tax_id": customer_details.get(inv.customer).get("tax_id"),
+ "customer_group": inv_customer_details.get("customer_group"),
+ "territory": inv_customer_details.get("territory"),
+ "tax_id": inv_customer_details.get("tax_id"),
"receivable_account": inv.debit_to,
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
"project": inv.project,
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index bbb8900de39..1ad24c46603 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -56,7 +56,7 @@ def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
):
if isinstance(boolean, str):
- boolean = frappe.json.loads(boolean)
+ boolean = loads(boolean)
fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 0f71e5d6f60..5ed62cb132f 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -652,7 +652,7 @@ frappe.ui.form.on("Asset", {
);
frm.set_value("gross_purchase_amount", purchase_amount);
- frm.set_value("purchase_receipt_amount", purchase_amount);
+ frm.set_value("purchase_amount", purchase_amount);
frm.set_value("asset_quantity", asset_quantity);
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
if (item.asset_location) {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 39a0867d984..3a2a942bdf2 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -72,7 +72,7 @@
"status",
"booked_fixed_asset",
"column_break_51",
- "purchase_receipt_amount",
+ "purchase_amount",
"default_finance_book",
"depr_entry_posting_status",
"amended_from",
@@ -408,15 +408,6 @@
"options": "Purchase Receipt",
"print_hide": 1
},
- {
- "fieldname": "purchase_receipt_amount",
- "fieldtype": "Currency",
- "hidden": 1,
- "label": "Purchase Receipt Amount",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- },
{
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"fieldname": "purchase_invoice",
@@ -546,6 +537,15 @@
"label": "Additional Asset Cost",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fieldname": "purchase_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Purchase Amount",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 72,
@@ -589,7 +589,7 @@
"link_fieldname": "target_asset"
}
],
- "modified": "2024-01-15 17:35:49.226603",
+ "modified": "2024-04-18 16:45:47.306032",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -633,4 +633,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 3a05a598b1c..063a5447ab5 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -92,10 +92,10 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None
+ purchase_amount: DF.Currency
purchase_date: DF.Date | None
purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None
- purchase_receipt_amount: DF.Currency
split_from: DF.Link | None
status: DF.Literal[
"Draft",
@@ -354,7 +354,7 @@ class Asset(AccountsController):
if self.is_existing_asset:
return
- if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
+ if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
error_message = _(
"Gross Purchase Amount should be equal to purchase amount of one single Asset."
)
@@ -696,7 +696,7 @@ class Asset(AccountsController):
purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
- if purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate():
+ if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
gl_entries.append(
self.get_gl_dict(
{
@@ -704,8 +704,8 @@ class Asset(AccountsController):
"against": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
- "credit": self.purchase_receipt_amount,
- "credit_in_account_currency": self.purchase_receipt_amount,
+ "credit": self.purchase_amount,
+ "credit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center,
},
item=self,
@@ -719,8 +719,8 @@ class Asset(AccountsController):
"against": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
- "debit": self.purchase_receipt_amount,
- "debit_in_account_currency": self.purchase_receipt_amount,
+ "debit": self.purchase_amount,
+ "debit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center,
},
item=self,
@@ -1116,8 +1116,8 @@ def create_new_asset_after_split(asset, split_qty):
)
new_asset.gross_purchase_amount = new_gross_purchase_amount
- if asset.purchase_receipt_amount:
- new_asset.purchase_receipt_amount = new_gross_purchase_amount
+ if asset.purchase_amount:
+ new_asset.purchase_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
@@ -1140,6 +1140,8 @@ def create_new_asset_after_split(asset, split_qty):
for row in new_asset.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
+ if not current_asset_depr_schedule_doc:
+ continue
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 1b3951e86a6..7e0c3ad6888 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -1000,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
- depreciation_amount = get_depreciation_amount(
+ depreciation_amount, prev_per_day_depr = get_depreciation_amount(
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
)
self.assertEqual(depreciation_amount, 30000)
@@ -1698,7 +1698,7 @@ def create_asset(**args):
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
- "purchase_receipt_amount": args.purchase_receipt_amount or 100000,
+ "purchase_amount": args.purchase_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06",
@@ -1723,6 +1723,7 @@ def create_asset(**args):
"depreciation_start_date": args.depreciation_start_date,
"daily_prorata_based": args.daily_prorata_based or 0,
"shift_based": args.shift_based or 0,
+ "rate_of_depreciation": args.rate_of_depreciation or 0,
},
)
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 8d3bcfc153d..573dd92c585 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -145,7 +145,7 @@ class AssetCapitalization(StockController):
def on_trash(self):
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
- super(AssetCapitalization, self).on_trash()
+ super().on_trash()
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
@@ -616,8 +616,7 @@ class AssetCapitalization(StockController):
asset_doc.available_for_use_date = self.posting_date
asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value
- asset_doc.purchase_receipt_amount = total_target_asset_value
- asset_doc.purchase_receipt_amount = total_target_asset_value
+ asset_doc.purchase_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.flags.asset_created_via_asset_capitalization = True
@@ -653,7 +652,7 @@ class AssetCapitalization(StockController):
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
- asset_doc.purchase_receipt_amount = total_target_asset_value
+ asset_doc.purchase_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.save()
diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
index 86a18c07d1f..31723ef3be3 100644
--- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
- self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
+ self.assertEqual(target_asset.purchase_amount, total_amount)
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
- self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
+ self.assertEqual(target_asset.purchase_amount, total_amount)
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
- self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
+ self.assertEqual(target_asset.purchase_amount, total_amount)
# Test General Ledger Entries
expected_gle = {
@@ -526,7 +526,7 @@ def create_depreciation_asset(**args):
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
asset.gross_purchase_amount = args.asset_value or 100000
- asset.purchase_receipt_amount = asset.gross_purchase_amount
+ asset.purchase_amount = asset.gross_purchase_amount
finance_book = asset.append("finance_books")
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index abbca68fea0..f64e9123dc0 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -285,6 +285,7 @@ class AssetDepreciationSchedule(Document):
number_of_pending_depreciations = final_number_of_depreciations - start
yearly_opening_wdv = value_after_depreciation
current_fiscal_year_end_date = None
+ prev_per_day_depr = True
for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
prev_depreciation_amount = 0
-
- depreciation_amount = get_depreciation_amount(
+ depreciation_amount, prev_per_day_depr = get_depreciation_amount(
self,
asset_doc,
value_after_depreciation,
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
number_of_pending_depreciations,
+ prev_per_day_depr,
)
if not has_pro_rata or (
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
@@ -362,6 +363,16 @@ class AssetDepreciationSchedule(Document):
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
+ if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
+ frappe.throw(
+ _(
+ "Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
+ ).format(
+ frappe.bold(asset_doc.gross_purchase_amount),
+ frappe.bold(row.total_number_of_depreciations),
+ frappe.bold(row.frequency_of_depreciation),
+ )
+ )
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
from_date = get_last_day(
@@ -599,11 +610,12 @@ def get_depreciation_amount(
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
number_of_pending_depreciations=0,
+ prev_per_day_depr=0,
):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
- )
+ ), None
else:
return get_wdv_or_dd_depr_amount(
asset,
@@ -614,6 +626,7 @@ def get_depreciation_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
+ prev_per_day_depr,
)
@@ -637,49 +650,14 @@ def get_straight_line_or_manual_depr_amount(
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_prorata_based:
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- total_days = (
- date_diff(
- get_last_day(
- add_months(
- row.depreciation_start_date,
- flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
- * row.frequency_of_depreciation,
- )
- ),
- add_days(
- get_last_day(
- add_months(
- row.depreciation_start_date,
- flt(
- row.total_number_of_depreciations
- - asset.number_of_depreciations_booked
- - number_of_pending_depreciations
- - 1
- )
- * row.frequency_of_depreciation,
- )
- ),
- 1,
- ),
- )
- + 1
- )
- daily_depr_amount = amount / total_days
-
- to_date = get_last_day(
- add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
+ return get_daily_prorata_based_straight_line_depr(
+ asset,
+ row,
+ schedule_idx,
+ number_of_pending_depreciations,
+ amount,
)
- from_date = add_days(
- get_last_day(
- add_months(
- row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
- )
- ),
- 1,
- )
-
- return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
@@ -692,40 +670,9 @@ def get_straight_line_or_manual_depr_amount(
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
)
-
- total_days = (
- date_diff(
- get_last_day(
- add_months(
- row.depreciation_start_date,
- flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
- * row.frequency_of_depreciation,
- )
- ),
- add_days(
- get_last_day(
- add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
- ),
- 1,
- ),
- )
- + 1
+ return get_daily_prorata_based_straight_line_depr(
+ asset, row, schedule_idx, number_of_pending_depreciations, amount
)
-
- daily_depr_amount = amount / total_days
-
- to_date = get_last_day(
- add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
- )
- from_date = add_days(
- get_last_day(
- add_months(
- row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
- )
- ),
- 1,
- )
- return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(asset.gross_purchase_amount)
@@ -734,6 +681,23 @@ def get_straight_line_or_manual_depr_amount(
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+def get_daily_prorata_based_straight_line_depr(
+ asset, row, schedule_idx, number_of_pending_depreciations, amount
+):
+ total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
+ every_year_depr = amount / total_years
+
+ year_start_date = add_years(
+ row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
+ )
+ year_end_date = add_days(add_years(year_start_date, 1), -1)
+ daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
+ from_date, total_depreciable_days = _get_total_days(
+ row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
+ )
+ return daily_depr_amount * total_depreciable_days
+
+
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
return (
@@ -779,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
+ prev_per_day_depr,
):
return get_default_wdv_or_dd_depr_amount(
asset,
@@ -788,6 +753,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
+ prev_per_day_depr,
)
@@ -799,6 +765,39 @@ def get_default_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
+ prev_per_day_depr,
+):
+ if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
+ return _get_default_wdv_or_dd_depr_amount(
+ asset,
+ fb_row,
+ depreciable_value,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ asset_depr_schedule,
+ ), None
+ else:
+ return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
+ asset,
+ fb_row,
+ depreciable_value,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ asset_depr_schedule,
+ prev_per_day_depr,
+ )
+
+
+def _get_default_wdv_or_dd_depr_amount(
+ asset,
+ fb_row,
+ depreciable_value,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ asset_depr_schedule,
):
if cint(fb_row.frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
@@ -825,6 +824,75 @@ def get_default_wdv_or_dd_depr_amount(
return prev_depreciation_amount
+def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
+ asset,
+ fb_row,
+ depreciable_value,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ asset_depr_schedule,
+ prev_per_day_depr,
+):
+ if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
+ if schedule_idx == 0:
+ return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
+
+ elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
+ return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
+ else:
+ return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
+ else:
+ if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
+ return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
+ else:
+ return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
+
+
+def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
+ """ "
+ Returns monthly depreciation amount when year changes
+ 1. Calculate per day depr based on new year
+ 2. Calculate monthly amount based on new per day amount
+ """
+ from_date, days_in_month = _get_total_days(
+ fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
+ )
+ per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
+ return (per_day_depr * days_in_month), per_day_depr
+
+
+def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
+ """ "
+ Returns monthly depreciation amount based on prev per day depr
+ Calculate per day depr only for the first month
+ """
+ from_date, days_in_month = _get_total_days(
+ fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
+ )
+ return (prev_per_day_depr * days_in_month), prev_per_day_depr
+
+
+def get_per_day_depr(
+ fb_row,
+ depreciable_value,
+ from_date,
+):
+ to_date = add_days(add_years(from_date, 1), -1)
+ total_days = date_diff(to_date, from_date) + 1
+ per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
+ return per_day_depr
+
+
+def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
+ from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
+ to_date = add_months(from_date, frequency_of_depreciation)
+ if is_last_day_of_the_month(depreciation_start_date):
+ to_date = get_last_day(to_date)
+ from_date = add_days(get_last_day(from_date), 1)
+ return from_date, date_diff(to_date, from_date) + 1
+
+
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
asset_depr_schedules_names = []
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
index c55063f2ebf..6e4966ac6cf 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
@@ -3,10 +3,12 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from frappe.utils import cstr
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
+ get_depr_schedule,
)
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
)
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
+
+ def test_daily_prorata_based_depr_on_sl_methond(self):
+ asset = create_asset(
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ daily_prorata_based=1,
+ available_for_use_date="2020-01-01",
+ depreciation_start_date="2020-01-31",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=24,
+ )
+
+ expected_schedules = [
+ ["2020-01-31", 4234.97, 4234.97],
+ ["2020-02-29", 3961.75, 8196.72],
+ ["2020-03-31", 4234.97, 12431.69],
+ ["2020-04-30", 4098.36, 16530.05],
+ ["2020-05-31", 4234.97, 20765.02],
+ ["2020-06-30", 4098.36, 24863.38],
+ ["2020-07-31", 4234.97, 29098.35],
+ ["2020-08-31", 4234.97, 33333.32],
+ ["2020-09-30", 4098.36, 37431.68],
+ ["2020-10-31", 4234.97, 41666.65],
+ ["2020-11-30", 4098.36, 45765.01],
+ ["2020-12-31", 4234.97, 49999.98],
+ ["2021-01-31", 4246.58, 54246.56],
+ ["2021-02-28", 3835.62, 58082.18],
+ ["2021-03-31", 4246.58, 62328.76],
+ ["2021-04-30", 4109.59, 66438.35],
+ ["2021-05-31", 4246.58, 70684.93],
+ ["2021-06-30", 4109.59, 74794.52],
+ ["2021-07-31", 4246.58, 79041.1],
+ ["2021-08-31", 4246.58, 83287.68],
+ ["2021-09-30", 4109.59, 87397.27],
+ ["2021-10-31", 4246.58, 91643.85],
+ ["2021-11-30", 4109.59, 95753.44],
+ ["2021-12-31", 4246.56, 100000.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Draft")
+ ]
+ self.assertEqual(schedules, expected_schedules)
+
+ # Test for Written Down Value Method
+ # Frequency of deprciation = 3
+ def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ depreciation_method="Written Down Value",
+ daily_prorata_based=1,
+ available_for_use_date="2021-02-20",
+ depreciation_start_date="2021-03-31",
+ frequency_of_depreciation=3,
+ total_number_of_depreciations=6,
+ rate_of_depreciation=40,
+ )
+
+ expected_schedules = [
+ ["2021-03-31", 4383.56, 4383.56],
+ ["2021-06-30", 9535.45, 13919.01],
+ ["2021-09-30", 9640.23, 23559.24],
+ ["2021-12-31", 9640.23, 33199.47],
+ ["2022-03-31", 9430.66, 42630.13],
+ ["2022-06-30", 5721.27, 48351.4],
+ ["2022-08-20", 51648.6, 100000.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Draft")
+ ]
+ self.assertEqual(schedules, expected_schedules)
+
+ # Frequency of deprciation = 6
+ def test_for_daily_prorata_based_depreciation_wdv_method_frequency_6_months(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ depreciation_method="Written Down Value",
+ daily_prorata_based=1,
+ available_for_use_date="2020-02-20",
+ depreciation_start_date="2020-02-29",
+ frequency_of_depreciation=6,
+ total_number_of_depreciations=6,
+ rate_of_depreciation=40,
+ )
+
+ expected_schedules = [
+ ["2020-02-29", 1092.90, 1092.90],
+ ["2020-08-31", 19944.01, 21036.91],
+ ["2021-02-28", 19618.83, 40655.74],
+ ["2021-08-31", 11966.4, 52622.14],
+ ["2022-02-28", 11771.3, 64393.44],
+ ["2022-08-31", 7179.84, 71573.28],
+ ["2023-02-20", 28426.72, 100000.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Draft")
+ ]
+ self.assertEqual(schedules, expected_schedules)
+
+ # Frequency of deprciation = 12
+ def test_for_daily_prorata_based_depreciation_wdv_method_frequency_12_months(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ depreciation_method="Written Down Value",
+ daily_prorata_based=1,
+ available_for_use_date="2020-02-20",
+ depreciation_start_date="2020-03-31",
+ frequency_of_depreciation=12,
+ total_number_of_depreciations=4,
+ rate_of_depreciation=40,
+ )
+
+ expected_schedules = [
+ ["2020-03-31", 4480.87, 4480.87],
+ ["2021-03-31", 38207.65, 42688.52],
+ ["2022-03-31", 22924.59, 65613.11],
+ ["2023-03-31", 13754.76, 79367.87],
+ ["2024-02-20", 20632.13, 100000],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Draft")
+ ]
+ self.assertEqual(schedules, expected_schedules)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 4ff04c80434..5d4ef4e3845 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -159,8 +159,9 @@ def prepare_chart_data(data, filters):
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
filters_filter_based_on = "Date Range"
date_field = "purchase_date"
- filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
- filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
+ filtered_data = [d for d in data if d.get(date_field)]
+ filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
+ filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
else:
filters_filter_based_on = filters.filter_based_on
date_field = frappe.scrub(filters.date_based_on)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5351ff6d791..a280724193d 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -794,6 +794,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"field_map": {
"name": "po_detail",
"parent": "purchase_order",
+ "material_request": "material_request",
+ "material_request_item": "material_request_item",
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
@@ -902,12 +904,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
)
target_doc.populate_items_table()
+ source_doc = frappe.get_doc("Purchase Order", source_name)
if target_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = target_doc.set_warehouse
else:
- source_doc = frappe.get_doc("Purchase Order", source_name)
if source_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = source_doc.set_warehouse
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 272d077b1e3..b8689d29a56 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
args: {
doctype: "Supplier",
- tag: args.tag,
+ tag: "%" + args.tag + "%",
},
callback: load_suppliers,
});
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
index 366fff191a0..56684a8659b 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
- frappe.query_reports["Purchase Order Trends"] = {
- filters: erpnext.get_purchase_trends_filters(),
- };
-});
+frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c527a02376c..40ce8b6bc57 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2183,10 +2183,10 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
- grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
+ grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
)
d.base_payment_amount = flt(
- base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
+ base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
elif not d.invoice_portion:
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 58bcc8c5fbe..bf6e3cd663a 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address
from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime
+import erpnext
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
@@ -332,6 +333,8 @@ class BuyingController(SubcontractingController):
else:
item.valuation_rate = 0.0
+ update_regional_item_valuation_rate(self)
+
def set_incoming_rate(self):
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
return
@@ -787,7 +790,7 @@ class BuyingController(SubcontractingController):
"supplier": self.supplier,
"purchase_date": self.posting_date,
"calculate_depreciation": 0,
- "purchase_receipt_amount": purchase_amount,
+ "purchase_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
@@ -935,3 +938,8 @@ def validate_item_type(doc, fieldname, message):
).format(items, message)
frappe.throw(error_message)
+
+
+@erpnext.allow_regional
+def update_regional_item_valuation_rate(doc):
+ pass
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 854682bec66..a07a00dd03e 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -352,6 +352,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
doctype = "Batch"
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
+ page_len = 30
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
@@ -422,6 +423,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
& (stock_ledger_entry.batch_no.isnotnull())
)
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
+ .having(Sum(stock_ledger_entry.actual_qty) != 0)
.offset(start)
.limit(page_len)
)
@@ -472,6 +474,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
)
.groupby(bundle.batch_no, bundle.warehouse)
+ .having(Sum(bundle.qty) != 0)
.offset(start)
.limit(page_len)
)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 913665dce2b..b61d1773a67 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from collections import defaultdict
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import flt, format_datetime, get_datetime
+from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty
+ target_doc.return_qty_from_rejected_warehouse = 1
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
- if source_doc.item_code:
+ if (
+ (source_doc.serial_no or source_doc.batch_no)
+ and not source_doc.serial_and_batch_bundle
+ and not source_doc.use_serial_batch_fields
+ ):
+ target_doc.set("use_serial_batch_fields", 1)
+
+ if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if not item_details.has_batch_no and not item_details.has_serial_no:
return
- if not target_doc.get("use_serial_batch_fields"):
- for qty_field in ["stock_qty", "rejected_qty"]:
- if not target_doc.get(qty_field):
- continue
-
- update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
- elif target_doc.get("use_serial_batch_fields"):
- update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
+ update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -839,3 +841,246 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
batches.update(get_batches_from_bundle(ids))
return batches
+
+
+def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
+ available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
+ if not available_dict:
+ frappe.throw(_("No Serial / Batches are available for return"))
+
+ return available_dict
+
+
+def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
+ _bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
+ if not _bundle_ids:
+ return frappe._dict({})
+
+ return get_serial_batches_based_on_bundle(field, _bundle_ids)
+
+
+def get_serial_batches_based_on_bundle(field, _bundle_ids):
+ available_dict = frappe._dict({})
+ batch_serial_nos = frappe.get_all(
+ "Serial and Batch Bundle",
+ fields=[
+ "`tabSerial and Batch Entry`.`serial_no`",
+ "`tabSerial and Batch Entry`.`batch_no`",
+ "`tabSerial and Batch Entry`.`qty`",
+ "`tabSerial and Batch Bundle`.`voucher_detail_no`",
+ "`tabSerial and Batch Bundle`.`voucher_type`",
+ "`tabSerial and Batch Bundle`.`voucher_no`",
+ ],
+ filters=[
+ ["Serial and Batch Bundle", "name", "in", _bundle_ids],
+ ["Serial and Batch Entry", "docstatus", "=", 1],
+ ],
+ order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
+ )
+
+ for row in batch_serial_nos:
+ key = row.voucher_detail_no
+ if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
+ key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
+
+ if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
+ row.qty = -1 * row.qty
+
+ if key not in available_dict:
+ available_dict[key] = frappe._dict(
+ {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
+ )
+
+ available_dict[key]["qty"] += row.qty
+
+ if row.serial_no:
+ available_dict[key]["serial_nos"][row.serial_no] += row.qty
+ elif row.batch_no:
+ available_dict[key]["batches"][row.batch_no] += row.qty
+
+ return available_dict
+
+
+def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
+ filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
+
+ pluck_field = "serial_and_batch_bundle"
+ if is_rejected:
+ del filters["serial_and_batch_bundle"]
+ filters["rejected_serial_and_batch_bundle"] = ("is", "set")
+ pluck_field = "rejected_serial_and_batch_bundle"
+
+ _bundle_ids = frappe.get_all(
+ doctype,
+ filters=filters,
+ pluck=pluck_field,
+ )
+
+ if not _bundle_ids:
+ return {}
+
+ del filters["name"]
+
+ filters[field] = ("in", reference_ids)
+
+ if not is_rejected:
+ _bundle_ids.extend(
+ frappe.get_all(
+ doctype,
+ filters=filters,
+ pluck="serial_and_batch_bundle",
+ )
+ )
+ else:
+ fields = [
+ "serial_and_batch_bundle",
+ ]
+
+ if is_rejected:
+ fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
+
+ del filters["rejected_serial_and_batch_bundle"]
+ data = frappe.get_all(
+ doctype,
+ fields=fields,
+ filters=filters,
+ )
+
+ for d in data:
+ if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"):
+ continue
+
+ if is_rejected:
+ if d.get("return_qty_from_rejected_warehouse"):
+ _bundle_ids.append(d.get("serial_and_batch_bundle"))
+ else:
+ _bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
+ else:
+ _bundle_ids.append(d.get("serial_and_batch_bundle"))
+
+ return _bundle_ids
+
+
+def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
+ if not qty_field:
+ qty_field = "qty"
+
+ if not warehouse_field:
+ warehouse_field = "warehouse"
+
+ warehouse = row.get(warehouse_field)
+ qty = abs(row.get(qty_field))
+
+ filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
+
+ if data.serial_nos:
+ available_serial_nos = []
+ for serial_no, sn_qty in data.serial_nos.items():
+ if sn_qty != 0:
+ available_serial_nos.append(serial_no)
+
+ if available_serial_nos:
+ if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
+ available_serial_nos = get_available_serial_nos(available_serial_nos)
+
+ if len(available_serial_nos) > qty:
+ filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
+ else:
+ filterd_serial_batch["serial_nos"] = available_serial_nos
+
+ elif data.batches:
+ for batch_no, batch_qty in data.batches.items():
+ if parent_doc.get("is_internal_customer"):
+ batch_qty = batch_qty * -1
+
+ if batch_qty <= 0:
+ continue
+
+ if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
+ batch_qty = get_available_batch_qty(
+ parent_doc,
+ batch_no,
+ warehouse,
+ )
+
+ if batch_qty <= 0:
+ frappe.throw(
+ _("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
+ title=_("Batch Not Available for Return"),
+ )
+
+ if qty <= 0:
+ break
+
+ if batch_qty > qty:
+ filterd_serial_batch["batches"][batch_no] = qty
+ qty = 0
+ else:
+ filterd_serial_batch["batches"][batch_no] += batch_qty
+ qty -= batch_qty
+
+ return filterd_serial_batch
+
+
+def get_available_batch_qty(parent_doc, batch_no, warehouse):
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ return get_batch_qty(
+ batch_no,
+ warehouse,
+ posting_date=parent_doc.posting_date,
+ posting_time=parent_doc.posting_time,
+ for_stock_levels=True,
+ )
+
+
+def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ type_of_transaction = "Outward"
+ if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
+ type_of_transaction = "Inward"
+
+ if not warehouse_field:
+ warehouse_field = "warehouse"
+
+ if not qty_field:
+ qty_field = "qty"
+
+ warehouse = child_doc.get(warehouse_field)
+ if parent_doc.get("is_internal_customer"):
+ warehouse = child_doc.get("target_warehouse")
+ type_of_transaction = "Outward"
+
+ if not child_doc.get(qty_field):
+ frappe.throw(
+ _("For the {0}, the quantity is required to make the return entry").format(
+ frappe.bold(child_doc.item_code)
+ )
+ )
+
+ cls_obj = SerialBatchCreation(
+ {
+ "type_of_transaction": type_of_transaction,
+ "item_code": child_doc.item_code,
+ "warehouse": warehouse,
+ "serial_nos": data.get("serial_nos"),
+ "batches": data.get("batches"),
+ "posting_date": parent_doc.posting_date,
+ "posting_time": parent_doc.posting_time,
+ "voucher_type": parent_doc.doctype,
+ "voucher_no": parent_doc.name,
+ "voucher_detail_no": child_doc.name,
+ "qty": child_doc.get(qty_field),
+ "company": parent_doc.company,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ return cls_obj.name
+
+
+def get_available_serial_nos(serial_nos, warehouse):
+ return frappe.get_all(
+ "Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
+ )
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 49252bd5b54..212c9adf4af 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
)
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
+from erpnext.controllers.sales_and_purchase_return import (
+ available_serial_batch_for_return,
+ filter_serial_batches,
+ make_serial_batch_bundle_for_return,
+)
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
@@ -92,7 +97,7 @@ class StockController(AccountsController):
)
)
- def make_gl_entries(self, gl_entries=None, from_repost=False):
+ def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -113,7 +118,11 @@ class StockController(AccountsController):
if self.docstatus == 1:
if not gl_entries:
- gl_entries = self.get_gl_entries(warehouse_account)
+ gl_entries = (
+ self.get_gl_entries(warehouse_account, via_landed_cost_voucher)
+ if self.doctype == "Purchase Receipt"
+ else self.get_gl_entries(warehouse_account)
+ )
make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self):
@@ -205,6 +214,7 @@ class StockController(AccountsController):
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"use_serial_batch_fields": row.use_serial_batch_fields,
+ "via_landed_cost_voucher": via_landed_cost_voucher,
"do_not_submit": True if not via_landed_cost_voucher else False,
}
@@ -216,6 +226,132 @@ class StockController(AccountsController):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row)
+ def make_bundle_for_sales_purchase_return(self, table_name=None):
+ if not self.get("is_return"):
+ return
+
+ if not table_name:
+ table_name = "items"
+
+ self.make_bundle_for_non_rejected_qty(table_name)
+
+ if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
+ self.make_bundle_for_rejected_qty(table_name)
+
+ def make_bundle_for_rejected_qty(self, table_name=None):
+ field, reference_ids = self.get_reference_ids(
+ table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
+ )
+
+ if not reference_ids:
+ return
+
+ child_doctype = self.doctype + " Item"
+ available_dict = available_serial_batch_for_return(
+ field, child_doctype, reference_ids, is_rejected=True
+ )
+
+ for row in self.get(table_name):
+ if data := available_dict.get(row.get(field)):
+ qty_field = "rejected_qty"
+ warehouse_field = "rejected_warehouse"
+ if row.get("return_qty_from_rejected_warehouse"):
+ qty_field = "qty"
+ warehouse_field = "warehouse"
+
+ if not data.get("qty"):
+ frappe.throw(
+ _("For the {0}, no stock is available for the return in the warehouse {1}.").format(
+ frappe.bold(row.item_code), row.get(warehouse_field)
+ )
+ )
+
+ data = filter_serial_batches(
+ self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
+ )
+ bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field, qty_field)
+ if row.get("return_qty_from_rejected_warehouse"):
+ row.db_set(
+ {
+ "serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "serial_no": "",
+ }
+ )
+ else:
+ row.db_set(
+ {
+ "rejected_serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "rejected_serial_no": "",
+ }
+ )
+
+ def make_bundle_for_non_rejected_qty(self, table_name):
+ field, reference_ids = self.get_reference_ids(table_name)
+ if not reference_ids:
+ return
+
+ child_doctype = self.doctype + " Item"
+ available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
+
+ for row in self.get(table_name):
+ if data := available_dict.get(row.get(field)):
+ data = filter_serial_batches(self, data, row)
+ bundle = make_serial_batch_bundle_for_return(data, row, self)
+ row.db_set(
+ {
+ "serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "serial_no": "",
+ }
+ )
+
+ def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
+ field = {
+ "Sales Invoice": "sales_invoice_item",
+ "Delivery Note": "dn_detail",
+ "Purchase Receipt": "purchase_receipt_item",
+ "Purchase Invoice": "purchase_invoice_item",
+ "POS Invoice": "pos_invoice_item",
+ }.get(self.doctype)
+
+ if not bundle_field:
+ bundle_field = "serial_and_batch_bundle"
+
+ if not qty_field:
+ qty_field = "qty"
+
+ reference_ids = []
+
+ for row in self.get(table_name):
+ if not self.is_serial_batch_item(row.item_code):
+ continue
+
+ if (
+ row.get(field)
+ and (
+ qty_field == "qty"
+ and not row.get("return_qty_from_rejected_warehouse")
+ or qty_field == "rejected_qty"
+ and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
+ )
+ and not row.get("use_serial_batch_fields")
+ and not row.get(bundle_field)
+ ):
+ reference_ids.append(row.get(field))
+
+ return field, reference_ids
+
+ @frappe.request_cache
+ def is_serial_batch_item(self, item_code) -> bool:
+ item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
+
+ if item_details.has_serial_no or item_details.has_batch_no:
+ return True
+
+ return False
+
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -590,6 +726,9 @@ class StockController(AccountsController):
row.db_set("rejected_serial_and_batch_bundle", None)
+ if row.get("current_serial_and_batch_bundle"):
+ row.db_set("current_serial_and_batch_bundle", None)
+
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
if not table_name:
table_name = "items"
@@ -610,35 +749,16 @@ class StockController(AccountsController):
def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
):
- bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
-
- if not type_of_transaction:
- type_of_transaction = "Inward"
-
- bundle_doc = frappe.copy_doc(bundle_doc)
- bundle_doc.warehouse = warehouse
- bundle_doc.type_of_transaction = type_of_transaction
- bundle_doc.voucher_type = self.doctype
- bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
- bundle_doc.is_cancelled = 0
-
- for row in bundle_doc.entries:
- row.is_outward = 0
- row.qty = abs(row.qty)
- row.stock_value_difference = abs(row.stock_value_difference)
- if type_of_transaction == "Outward":
- row.qty *= -1
- row.stock_value_difference *= row.stock_value_difference
- row.is_outward = 1
-
- row.warehouse = warehouse
-
- bundle_doc.calculate_qty_and_amount()
- bundle_doc.flags.ignore_permissions = True
- bundle_doc.flags.ignore_validate = True
- bundle_doc.save(ignore_permissions=True)
-
- return bundle_doc.name
+ return make_bundle_for_material_transfer(
+ is_new=self.is_new(),
+ docstatus=self.docstatus,
+ voucher_type=self.doctype,
+ voucher_no=self.name,
+ serial_and_batch_bundle=serial_and_batch_bundle,
+ warehouse=warehouse,
+ type_of_transaction=type_of_transaction,
+ do_not_submit=do_not_submit,
+ )
def get_sl_entries(self, d, args):
sl_dict = frappe._dict(
@@ -1556,3 +1676,38 @@ def create_item_wise_repost_entries(
repost_entries.append(repost_entry)
return repost_entries
+
+
+def make_bundle_for_material_transfer(**kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
+
+ if not kwargs.type_of_transaction:
+ kwargs.type_of_transaction = "Inward"
+
+ bundle_doc = frappe.copy_doc(bundle_doc)
+ bundle_doc.warehouse = kwargs.warehouse
+ bundle_doc.type_of_transaction = kwargs.type_of_transaction
+ bundle_doc.voucher_type = kwargs.voucher_type
+ bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
+ bundle_doc.is_cancelled = 0
+
+ for row in bundle_doc.entries:
+ row.is_outward = 0
+ row.qty = abs(row.qty)
+ row.stock_value_difference = abs(row.stock_value_difference)
+ if kwargs.type_of_transaction == "Outward":
+ row.qty *= -1
+ row.stock_value_difference *= row.stock_value_difference
+ row.is_outward = 1
+
+ row.warehouse = kwargs.warehouse
+
+ bundle_doc.calculate_qty_and_amount()
+ bundle_doc.flags.ignore_permissions = True
+ bundle_doc.flags.ignore_validate = True
+ bundle_doc.save(ignore_permissions=True)
+
+ return bundle_doc.name
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index fc66345aeee..e0f1ed77140 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
consumed_bundles.batch_nos[batch_no] += abs(qty)
# Will be deprecated in v16
- if row.serial_no:
+ if row.serial_no and not consumed_bundles.serial_nos:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
# Will be deprecated in v16
- if row.batch_no:
+ if row.batch_no and not consumed_bundles.batch_nos:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def get_available_materials(self):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index aeaebafabe9..db9d31b53bb 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
self.title = self.customer_name
self.calculate_totals()
+
+ def on_update(self):
self.update_prospect()
def map_fields(self):
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 4ce8342af6e..58ce7c50583 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -591,13 +591,13 @@
"default": "0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
- "label": "FG based Operating Cost"
+ "label": "Finished Goods based Operating Cost"
},
{
"depends_on": "fg_based_operating_cost",
"fieldname": "fg_based_section_section",
"fieldtype": "Section Break",
- "label": "FG Based Operating Cost Section"
+ "label": "Finished Goods Based Operating Cost"
},
{
"depends_on": "fg_based_operating_cost",
@@ -637,7 +637,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2024-04-02 16:22:47.518411",
+ "modified": "2024-06-03 16:24:47.518411",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index 0158f7c5b97..e236e7a6345 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -80,6 +80,18 @@ class BOMCreator(Document):
if row.is_expandable and row.item_code == self.item_code:
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
+ if not row.parent_row_no and row.fg_item and row.fg_item != self.item_code:
+ frappe.throw(
+ _("At row {0}: set Parent Row No for item {1}").format(row.idx, row.item_code),
+ title=_("Set Parent Row No in Items Table"),
+ )
+
+ elif row.parent_row_no and row.fg_item == self.item_code:
+ frappe.throw(
+ _("At row {0}: Parent Row No cannot be set for item {1}").format(row.idx, row.item_code),
+ title=_("Remove Parent Row No in Items Table"),
+ )
+
def set_status(self, save=False):
self.status = {
0: "Draft",
@@ -410,6 +422,10 @@ def add_sub_assembly(**kwargs):
parent_row_no = item_row.idx
name = ""
+ else:
+ parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
+ if parent_row_no:
+ parent_row_no = parent_row_no[0]
for row in bom_item.get("items"):
row = frappe._dict(row)
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index 56acd8a1a67..1726f898751 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -70,7 +70,7 @@
"fieldname": "fg_item",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "FG Item",
+ "label": "Finished Goods Item",
"options": "Item",
"reqd": 1
},
@@ -203,7 +203,7 @@
{
"fieldname": "fg_reference_id",
"fieldtype": "Data",
- "label": "FG Reference",
+ "label": "Finished Goods Reference",
"no_copy": 1,
"read_only": 1
},
@@ -230,7 +230,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-16 13:34:06.321061",
+ "modified": "2024-06-03 18:45:24.339532",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 5a734d8684f..037a3fe0e81 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -77,7 +77,7 @@
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Operation Time ",
+ "label": "Operation Time",
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"reqd": 1
@@ -203,4 +203,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 4fd628bbecb..abea4c86279 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -214,7 +214,11 @@ class JobCard(Document):
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
- data = self.get_overlap_for(d)
+ open_job_cards = []
+ if d.get("employee"):
+ open_job_cards = self.get_open_job_cards(d.get("employee"))
+
+ data = self.get_overlap_for(d, open_job_cards=open_job_cards)
if data:
frappe.throw(
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
@@ -235,12 +239,12 @@ class JobCard(Document):
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
- def get_overlap_for(self, args):
+ def get_overlap_for(self, args, open_job_cards=None):
time_logs = []
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
- time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
+ time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", open_job_cards=open_job_cards))
if not time_logs:
return {}
@@ -304,7 +308,7 @@ class JobCard(Document):
return True
return overlap
- def get_time_logs(self, args, doctype):
+ def get_time_logs(self, args, doctype, open_job_cards=None):
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType(doctype)
@@ -341,8 +345,14 @@ class JobCard(Document):
if self.workstation:
query = query.where(jc.workstation == self.workstation)
- if args.get("employee") and doctype == "Job Card Time Log":
- query = query.where(jctl.employee == args.get("employee"))
+ if args.get("employee"):
+ if not open_job_cards and doctype == "Job Card Scheduled Time":
+ return []
+
+ if doctype == "Job Card Time Log":
+ query = query.where(jctl.employee == args.get("employee"))
+ else:
+ query = query.where(jc.name.isin(open_job_cards))
if doctype != "Job Card Time Log":
query = query.where(jc.total_time_in_mins == 0)
@@ -351,6 +361,27 @@ class JobCard(Document):
return time_logs
+ def get_open_job_cards(self, employee):
+ jc = frappe.qb.DocType("Job Card")
+ jctl = frappe.qb.DocType("Job Card Time Log")
+
+ query = (
+ frappe.qb.from_(jc)
+ .left_join(jctl)
+ .on(jc.name == jctl.parent)
+ .select(jc.name)
+ .where(
+ (jctl.parent == jc.name)
+ & (jc.workstation == self.workstation)
+ & (jctl.employee == employee)
+ & (jc.docstatus < 1)
+ & (jc.name != self.name)
+ )
+ )
+
+ jobs = query.run(as_dict=True)
+ return [job.get("name") for job in jobs] if jobs else []
+
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
workstations = get_workstations(self.workstation_type)
if workstations:
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index a7102d7d237..6a86214986a 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -42,8 +42,7 @@
"fieldname": "completed_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Completed Qty",
- "reqd": 1
+ "label": "Completed Qty"
},
{
"fieldname": "employee",
@@ -64,7 +63,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-23 14:30:00.970916",
+ "modified": "2024-05-21 12:40:55.765860",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",
@@ -74,4 +73,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 6db901c71a4..5b4ef233926 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -400,13 +400,20 @@ frappe.ui.form.on("Production Plan", {
},
download_materials_required(frm) {
+ const warehouses_data = [];
+
+ if (frm.doc.for_warehouse) {
+ warehouses_data.push({ warehouse: frm.doc.for_warehouse });
+ }
+
const fields = [
{
fieldname: "warehouses",
fieldtype: "Table MultiSelect",
label: __("Warehouses"),
- default: frm.doc.from_warehouse,
+ default: warehouses_data,
options: "Production Plan Material Request Warehouse",
+ reqd: 1,
get_query: function () {
return {
filters: {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index a378d8ae606..7d3aa000c87 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -452,6 +452,10 @@ class ProductionPlan(Document):
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
)
+ bom_no = data.bom_no or item_details and item_details.bom_no or ""
+ if not bom_no:
+ continue
+
pi = self.append(
"po_items",
{
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
"item_code": data.item_code,
"description": data.description or item_details.description,
"stock_uom": item_details and item_details.stock_uom or "",
- "bom_no": data.bom_no or item_details and item_details.bom_no or "",
+ "bom_no": bom_no,
"planned_qty": data.pending_qty,
"pending_qty": data.pending_qty,
"planned_start_date": now_datetime(),
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 8957c11d3f0..71d9b59c677 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
+ def test_production_plan_with_non_active_bom_item(self):
+ item = make_item("Test Production Item 1 for Non Active BOM", {"is_stock_item": 1}).name
+
+ so1 = make_sales_order(item_code=item, qty=1)
+
+ pln = frappe.new_doc("Production Plan")
+ pln.company = so1.company
+ pln.get_items_from = "Sales Order"
+ pln.append(
+ "sales_orders",
+ {
+ "sales_order": so1.name,
+ "sales_order_date": so1.transaction_date,
+ "customer": so1.customer,
+ "grand_total": so1.grand_total,
+ },
+ )
+
+ pln.get_items()
+
+ self.assertFalse(pln.po_items)
+
def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = "Test Production Item 1"
diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
index 78a389760a7..089d7396b60 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -85,7 +85,7 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "FG Warehouse",
+ "label": "Finished Goods Warehouse",
"options": "Warehouse"
},
{
@@ -220,7 +220,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-02-27 13:24:43.571844",
+ "modified": "2024-06-03 13:10:20.252166",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index 2aa31be0f0e..97c85502c98 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all(
"BOM Item",
filters={"parent": bom},
- fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
+ fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
+ order_by="idx ASC",
)
for item in exploded_items:
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
index 23fa9ab41b0..4a34d126f88 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
},
},
],
+ formatter: function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
+ value = "" + value + "";
+ }
+ return value;
+ },
};
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index 85648d6b326..0f5fa959dc5 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
if not self.data:
return
- total_row = {"item_code": _(frappe.bold("Total Quantity"))}
+ total_row = {"item_code": _("Total Quantity")}
for value in self.data:
for period in self.period_list:
diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
index 51efc6e655f..73560dd939b 100644
--- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
@@ -102,7 +102,12 @@ def get_columns() -> Columns:
"fieldtype": "Float",
"width": "150",
},
- {"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
+ {
+ "label": _("Finished Goods Value"),
+ "fieldname": "total_fg_value",
+ "fieldtype": "Float",
+ "width": "150",
+ },
{
"label": _("Raw Material Value"),
"fieldname": "total_rm_value",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0465f2ca53e..2522077e9c3 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -363,3 +363,5 @@ erpnext.patches.v14_0.set_maintain_stock_for_bom_item
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
+erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
+erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py b/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py
index 61a5c86386c..a1719fb41bb 100644
--- a/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py
+++ b/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py
@@ -13,8 +13,9 @@ def execute():
for d in accounting_dimensions:
doctype = "Asset Repair"
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
+ docfield = frappe.db.get_value("DocField", {"parent": doctype, "fieldname": d.fieldname})
- if field:
+ if field or docfield:
continue
df = {
diff --git a/erpnext/patches/v14_0/enable_set_priority_for_pricing_rules.py b/erpnext/patches/v14_0/enable_set_priority_for_pricing_rules.py
new file mode 100644
index 00000000000..af87eeb2727
--- /dev/null
+++ b/erpnext/patches/v14_0/enable_set_priority_for_pricing_rules.py
@@ -0,0 +1,10 @@
+import frappe
+
+
+def execute():
+ pr_table = frappe.qb.DocType("Pricing Rule")
+ (
+ frappe.qb.update(pr_table)
+ .set(pr_table.has_priority, 1)
+ .where((pr_table.priority.isnotnull()) & (pr_table.priority != ""))
+ ).run()
diff --git a/erpnext/patches/v15_0/rename_purchase_receipt_amount_to_purchase_amount.py b/erpnext/patches/v15_0/rename_purchase_receipt_amount_to_purchase_amount.py
new file mode 100644
index 00000000000..8af3ed26323
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_purchase_receipt_amount_to_purchase_amount.py
@@ -0,0 +1,8 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ frappe.reload_doc("assets", "doctype", "asset")
+ if frappe.db.has_column("Asset", "purchase_receipt_amount"):
+ rename_field("Asset", "purchase_receipt_amount", "purchase_amount")
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 49e8d8486a5..d03ab786cc1 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -55,6 +55,14 @@ frappe.ui.form.on("Project", {
filters: filters,
};
});
+
+ frm.set_query("cost_center", () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ },
+ };
+ });
},
refresh: function (frm) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index a0685d80985..524d3033579 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1246,8 +1246,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
qty(doc, cdt, cdn) {
- if (!this.frm.doc.__onload?.load_after_mapping) {
- let item = frappe.get_doc(cdt, cdn);
+ let item = frappe.get_doc(cdt, cdn);
+ if (!this.is_a_mapped_document(item)) {
// item.pricing_rules = ''
frappe.run_serially([
() => this.remove_pricing_rule_for_item(item),
@@ -2295,6 +2295,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (doc.is_return) {
filters["is_return"] = 1;
+ if (["Sales Invoice", "Delivery Note"].includes(doc.doctype)) {
+ filters["is_inward"] = 1;
+ }
}
if (item.warehouse) filters["warehouse"] = item.warehouse;
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 527d452a450..6e1097072fa 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -34,5 +34,7 @@ import "./utils/sales_common.js";
import "./controllers/buying.js";
import "./utils/demo.js";
import "./financial_statements.js";
+import "./sales_trends_filters.js";
+import "./purchase_trends_filters.js";
// import { sum } from 'frappe/public/utils/util.js'
diff --git a/erpnext/public/js/purchase_trends_filters.js b/erpnext/public/js/purchase_trends_filters.js
index 14ffaf82162..75428d3be25 100644
--- a/erpnext/public/js/purchase_trends_filters.js
+++ b/erpnext/public/js/purchase_trends_filters.js
@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-erpnext.get_purchase_trends_filters = function () {
- return [
+erpnext.purchase_trends_filters = {
+ filters: [
{
fieldname: "company",
label: __("Company"),
@@ -63,5 +63,5 @@ erpnext.get_purchase_trends_filters = function () {
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
default: "",
},
- ];
+ ],
};
diff --git a/erpnext/public/js/sales_trends_filters.js b/erpnext/public/js/sales_trends_filters.js
index 85daa01ff67..2f8e6f93c61 100644
--- a/erpnext/public/js/sales_trends_filters.js
+++ b/erpnext/public/js/sales_trends_filters.js
@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-erpnext.get_sales_trends_filters = function () {
- return [
+erpnext.sales_trends_filters = {
+ filters: [
{
fieldname: "period",
label: __("Period"),
@@ -53,5 +53,5 @@ erpnext.get_sales_trends_filters = function () {
options: "Company",
default: frappe.defaults.get_user_default("Company"),
},
- ];
+ ],
};
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 288b2f6932d..a7d88edcafa 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -933,7 +933,13 @@ erpnext.utils.map_current_doc = function (opts) {
frappe.msgprint(__("Please select {0}", [opts.source_doctype]));
return;
}
- opts.source_name = values;
+
+ if (values.constructor === Array) {
+ opts.source_name = [...new Set(values)];
+ } else {
+ opts.source_name = values;
+ }
+
if (
opts.allow_child_item_selection ||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
@@ -1183,4 +1189,39 @@ $.extend(erpnext.stock.utils, {
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
barcode_scanner.scan_api_call(child_row.barcode, callback);
},
+
+ get_serial_range(range_string, separator) {
+ /* Return an array of serial numbers generated from a range string.
+
+ Examples (using separator "::"):
+ - "1::5" => ["1", "2", "3", "4", "5"]
+ - "SN0009::12" => ["SN0009", "SN0010", "SN0011", "SN0012"]
+ - "ABC//05::8" => ["ABC//05", "ABC//06", "ABC//07", "ABC//08"]
+ */
+ if (!range_string) {
+ return;
+ }
+
+ const [start_str, end_str] = range_string.trim().split(separator);
+
+ if (!start_str || !end_str) {
+ return;
+ }
+
+ const end_int = parseInt(end_str);
+ const length_difference = start_str.length - end_str.length;
+ const start_int = parseInt(start_str.substring(length_difference));
+
+ if (isNaN(start_int) || isNaN(end_int)) {
+ return;
+ }
+
+ const serial_numbers = Array(end_int - start_int + 1)
+ .fill(1)
+ .map((x, y) => x + y)
+ .map((x) => x + start_int - 1);
+ return serial_numbers.map((val) => {
+ return start_str.substring(0, length_difference) + val.toString().padStart(end_str.length, "0");
+ });
+ },
});
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 1edeca95018..4928f2dc1a5 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -206,6 +206,16 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label: __("{0} {1} Manually", [primary_label, label]),
depends_on: "eval:doc.import_using_csv_file === 0",
},
+ {
+ fieldtype: "Data",
+ label: __("Enter Serial No Range"),
+ fieldname: "serial_no_range",
+ depends_on: "eval:doc.import_using_csv_file === 0",
+ description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
+ onchange: () => {
+ this.set_serial_nos_from_range();
+ },
+ },
{
fieldtype: "Small Text",
label: __("Enter Serial Nos"),
@@ -255,6 +265,20 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
return fields;
}
+ set_serial_nos_from_range() {
+ const serial_no_range = this.dialog.get_value("serial_no_range");
+
+ if (!serial_no_range) {
+ return;
+ }
+
+ const serial_nos = erpnext.stock.utils.get_serial_range(serial_no_range, "::");
+
+ if (serial_nos) {
+ this.dialog.set_value("upload_serial_nos", serial_nos.join("\n"));
+ }
+ }
+
create_serial_nos() {
let { upload_serial_nos } = this.dialog.get_values();
@@ -589,7 +613,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
render_data() {
- if (this.bundle) {
+ if (this.bundle || this.frm.doc.is_return) {
frappe
.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers",
@@ -597,6 +621,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
item_code: this.item.item_code,
name: this.bundle,
voucher_no: !this.frm.is_new() ? this.item.parent : "",
+ child_row: this.frm.doc.is_return ? this.item : "",
},
})
.then((r) => {
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 63a44cc19fb..90051673e70 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
+import json
+
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
@@ -321,7 +323,7 @@ class TestCustomer(FrappeTestCase):
frappe.ValidationError,
update_child_qty_rate,
so.doctype,
- frappe.json.dumps([modified_item]),
+ json.dumps([modified_item]),
so.name,
)
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.js b/erpnext/selling/doctype/product_bundle/product_bundle.js
index 3096b692a7e..67b9ae5ba31 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.js
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.js
@@ -9,5 +9,13 @@ frappe.ui.form.on("Product Bundle", {
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
};
});
+
+ frm.set_query("item_code", "items", () => {
+ return {
+ filters: {
+ has_variants: 0,
+ },
+ };
+ });
},
});
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 8c37e2f088e..d7aff8632b9 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -903,6 +903,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
fields: fields,
primary_action: function () {
var data = { items: d.fields_dict.items.grid.get_selected_children() };
+ if (!data) {
+ frappe.throw(__("Please select items"));
+ }
me.frm.call({
method: "make_work_orders",
args: {
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 3a045d5ff2d..0695c3fd9c4 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1139,7 +1139,8 @@
"hide_seconds": 1,
"label": "Inter Company Order Reference",
"options": "Purchase Order",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "project",
@@ -1645,7 +1646,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-29 16:27:41.539613",
+ "modified": "2024-05-23 16:35:54.905804",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index bc8991486db..e5e67ed38b7 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -794,6 +794,11 @@ def get_requested_item_qty(sales_order):
def make_material_request(source_name, target_doc=None):
requested_item_qty = get_requested_item_qty(source_name)
+ def postprocess(source, target):
+ if source.tc_name and frappe.db.get_value("Terms and Conditions", source.tc_name, "buying") != 1:
+ target.tc_name = None
+ target.terms = None
+
def get_remaining_qty(so_item):
return flt(
flt(so_item.qty)
@@ -849,6 +854,7 @@ def make_material_request(source_name, target_doc=None):
},
},
target_doc,
+ postprocess,
)
return doc
@@ -1224,11 +1230,19 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
target.discount_amount = 0.0
target.inter_company_order_reference = ""
target.shipping_rule = ""
+ target.tc_name = ""
+ target.terms = ""
+ target.payment_terms_template = ""
+ target.payment_schedule = []
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
if default_price_list:
target.buying_price_list = default_price_list
+ default_payment_terms = frappe.get_value("Supplier", supplier, "payment_terms")
+ if default_payment_terms:
+ target.payment_terms_template = default_payment_terms
+
if any(item.delivered_by_supplier == 1 for item in source.items):
if source.shipping_address_name:
target.shipping_address = source.shipping_address_name
@@ -1280,7 +1294,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"contact_person",
"taxes_and_charges",
"shipping_address",
- "terms",
],
"validation": {"docstatus": ["=", 1]},
},
@@ -1348,6 +1361,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.discount_amount = 0.0
target.inter_company_order_reference = ""
target.shipping_rule = ""
+ target.tc_name = ""
+ target.terms = ""
+ target.payment_terms_template = ""
+ target.payment_schedule = []
if is_drop_ship_order(target):
target.customer = source.customer
@@ -1383,7 +1400,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"contact_person",
"taxes_and_charges",
"shipping_address",
- "terms",
],
"validation": {"docstatus": ["=", 1]},
},
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 452019ebf4b..864ceffa8b1 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -684,7 +684,7 @@ erpnext.PointOfSale.Controller = class {
const is_stock_item = resp[1];
frappe.dom.unfreeze();
- const bold_uom = item_row.stock_uom.bold();
+ const bold_uom = item_row.uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold();
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index fbee9c16267..694f70d4db5 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -857,7 +857,7 @@ erpnext.PointOfSale.ItemCart = class {
});
this.$customer_section.find(".customer-details").html(
`