Compare commits

...

44 Commits

Author SHA1 Message Date
Frappe PR Bot
79a4b597d4 chore(release): Bumped to Version 14.85.9
## [14.85.9](https://github.com/frappe/erpnext/compare/v14.85.8...v14.85.9) (2025-05-20)

### Bug Fixes

* asset image field updation issue (backport [#47615](https://github.com/frappe/erpnext/issues/47615)) ([d88feec](d88feecf46))
* better validation message with solution for BOM recursion (backport [#47472](https://github.com/frappe/erpnext/issues/47472)) ([#47476](https://github.com/frappe/erpnext/issues/47476)) ([537c917](537c917bfc))
* include only invoices with update_stock = 0  for billed amt in delivery note. ([54197ff](54197ff760))
* remove hardcoded doctype in `make_return_doc` ([9ce86b1](9ce86b135b))
2025-05-20 13:53:32 +00:00
ruthra kumar
0b0944cc06 Merge pull request #47635 from frappe/version-14-hotfix
chore: release v14
2025-05-20 19:22:04 +05:30
mergify[bot]
537c917bfc fix: better validation message with solution for BOM recursion (backport #47472) (#47476)
fix: better validation message with solution for BOM recursion

(cherry picked from commit 7103cdd84a)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2025-05-20 16:15:40 +05:30
ruthra kumar
13f393c59a Merge pull request #47624 from frappe/mergify/bp/version-14-hotfix/pr-47559
fix: include only invoices with update_stock = 0  for billed amt in delivery note. (backport #47559)
2025-05-20 11:24:52 +05:30
ljain112
54197ff760 fix: include only invoices with update_stock = 0 for billed amt in delivery note.
(cherry picked from commit 6dc459db58)
2025-05-20 05:31:56 +00:00
ruthra kumar
6f1109226d Merge pull request #47621 from frappe/mergify/bp/version-14-hotfix/pr-47614
fix: remove hardcoded doctype in `make_return_doc` (backport #47614)
2025-05-20 10:37:00 +05:30
barredterra
9ce86b135b fix: remove hardcoded doctype in make_return_doc
(cherry picked from commit 45a5c19dd4)

# Conflicts:
#	erpnext/controllers/sales_and_purchase_return.py
2025-05-20 10:11:21 +05:30
mergify[bot]
d88feecf46 fix: asset image field updation issue (backport #47615) 2025-05-20 10:08:26 +05:30
Frappe PR Bot
271d0a301f chore(release): Bumped to Version 14.85.8
## [14.85.8](https://github.com/frappe/erpnext/compare/v14.85.7...v14.85.8) (2025-05-13)

### Bug Fixes

* broken test suite due to incorrect OR filter ([949ed59](949ed59f84))
* ignore "Account Closing Balance" doctype on Period Closing Voucher cancellation ([a04feff](a04feff264))
* typo in event.js ([2389fd5](2389fd5145))
* warning message for COGS account in the stock entry ([1bbbd26](1bbbd261cb))
2025-05-13 14:02:11 +00:00
ruthra kumar
9d36166616 Merge pull request #47529 from frappe/version-14-hotfix
chore: release v14
2025-05-13 19:30:41 +05:30
ruthra kumar
00407cd0ee Merge pull request #47526 from frappe/mergify/bp/version-14-hotfix/pr-47520
fix: ignore "Account Closing Balance" doctype on Period Closing Voucher cancellation (backport #47520)
2025-05-13 15:22:32 +05:30
ljain112
a04feff264 fix: ignore "Account Closing Balance" doctype on Period Closing Voucher cancellation
(cherry picked from commit d6602d63fc)

# Conflicts:
#	erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
2025-05-13 15:00:36 +05:30
ruthra kumar
d6f5ac90b1 Merge pull request #47516 from frappe/mergify/bp/version-14-hotfix/pr-47367
fix: Use `Currency` instead of `Float` in GL report to show details (backport #47367)
2025-05-13 13:08:20 +05:30
Abdeali Chharchhodawala
c6c1b00559 Merge pull request #47367 from Abdeali099/gl-report-field-float-to-currency
fix: Use `Currency` instead of `Float` in GL report to show details
(cherry picked from commit e4e0bb68ec)
2025-05-13 12:49:52 +05:30
ruthra kumar
b86dc294f1 Merge pull request #47518 from frappe/mergify/bp/version-14-hotfix/pr-47380
fix: broken CI - uae vat 201 tests failing (backport #47380)
2025-05-13 12:45:41 +05:30
ruthra kumar
949ed59f84 fix: broken test suite due to incorrect OR filter
(cherry picked from commit 37d74e387d)
2025-05-13 06:39:22 +00:00
ruthra kumar
558225b027 Merge pull request #47466 from frappe/mergify/bp/version-14-hotfix/pr-47462
Update event.js (backport #47462)
2025-05-08 14:12:55 +05:30
Yaiphalemba Mangshatabam
2389fd5145 fix: typo in event.js
"Sales Partners" -> "Sales Partner"

(cherry picked from commit edee75c757)
2025-05-08 08:40:37 +00:00
rohitwaghchaure
d66a34411c Merge pull request #47453 from frappe/mergify/bp/version-14-hotfix/pr-47452
fix: warning message for COGS account in the stock entry (backport #47452)
2025-05-08 13:58:23 +05:30
Rohit Waghchaure
1bbbd261cb fix: warning message for COGS account in the stock entry
(cherry picked from commit bba6b0ff45)
2025-05-07 10:50:18 +00:00
Frappe PR Bot
182b9892fc chore(release): Bumped to Version 14.85.7
## [14.85.7](https://github.com/frappe/erpnext/compare/v14.85.6...v14.85.7) (2025-05-06)

### Bug Fixes

* backward compatibility for renamed group_by filter on reports (backport [#47362](https://github.com/frappe/erpnext/issues/47362)) ([#47402](https://github.com/frappe/erpnext/issues/47402)) ([378821d](378821d9e6))
* change shipping address fetching condition ([8e0bd97](8e0bd976c3))
* completed transactions showing in the list (backport [#47374](https://github.com/frappe/erpnext/issues/47374)) ([#47378](https://github.com/frappe/erpnext/issues/47378)) ([b507e63](b507e63375))
* do not allocate amount when ref's doctype or name are not set ([f278120](f278120aa0))
* rename unchanged group_by filter related to general ledger report (backport [#47366](https://github.com/frappe/erpnext/issues/47366)) ([#47404](https://github.com/frappe/erpnext/issues/47404)) ([d41fec7](d41fec7d2b))
* renaming group by fieldname and value in reports (backport [#47352](https://github.com/frappe/erpnext/issues/47352)) ([#47359](https://github.com/frappe/erpnext/issues/47359)) ([27a8856](27a8856dca))
* show party type in due date exceeding message ([4376fbc](4376fbc3ed))
* validation for difference account ([150cc5a](150cc5a664))
* warning message before changing the valuation method (backport [#47340](https://github.com/frappe/erpnext/issues/47340)) ([#47341](https://github.com/frappe/erpnext/issues/47341)) ([b13e0a6](b13e0a6b9f))
2025-05-06 14:13:22 +00:00
ruthra kumar
015525599f Merge pull request #47430 from frappe/version-14-hotfix
chore: release v14
2025-05-06 19:41:49 +05:30
mergify[bot]
3993525bf6 feat!: configure which rate is used to auto-update price list (backport #47417) (#47433)
* feat!: configure which rate is used to auto-update price list

(cherry picked from commit 3ebde4526a)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
#	erpnext/setup/setup_wizard/operations/defaults_setup.py
#	erpnext/setup/setup_wizard/operations/install_fixtures.py
#	erpnext/stock/doctype/stock_settings/stock_settings.json
#	erpnext/stock/doctype/stock_settings/stock_settings.py
#	erpnext/stock/get_item_details.py

* fix: merge conflicts

---------

Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
2025-05-06 18:43:27 +05:30
ruthra kumar
8343c92ac5 Merge pull request #47425 from frappe/mergify/bp/version-14-hotfix/pr-47337
fix: do not allocate amount when ref's doctype or name are not set (backport #47337)
2025-05-06 15:01:05 +05:30
Abdeali Chharchhoda
f278120aa0 fix: do not allocate amount when ref's doctype or name are not set
(cherry picked from commit b9a02b466b)
2025-05-06 09:04:52 +00:00
ruthra kumar
862e6330ac Merge pull request #47415 from frappe/mergify/bp/version-14-hotfix/pr-47408
fix: show party type in due date exceeding message (backport #47408)
2025-05-06 14:32:58 +05:30
ruthra kumar
bd395e2406 chore: resolve conflicts and pass all parameters 2025-05-06 14:12:44 +05:30
Abdeali Chharchhoda
4376fbc3ed fix: show party type in due date exceeding message
(cherry picked from commit b6d9134014)

# Conflicts:
#	erpnext/accounts/party.py
2025-05-06 06:28:47 +00:00
ruthra kumar
c49ee563fd Merge pull request #47413 from frappe/mergify/bp/version-14-hotfix/pr-47358
fix: change shipping address fetching condition (backport #47358)
2025-05-06 11:29:16 +05:30
Vimal
8e0bd976c3 fix: change shipping address fetching condition
(cherry picked from commit 0b4add2f2b)
2025-05-06 05:28:40 +00:00
rohitwaghchaure
d66e8e8597 Merge pull request #47389 from frappe/mergify/bp/version-14-hotfix/pr-47376
fix: validation for difference account (backport #47376)
2025-05-05 18:39:04 +05:30
mergify[bot]
d41fec7d2b fix: rename unchanged group_by filter related to general ledger report (backport #47366) (#47404)
* fix: rename unchanged group_by filter related to general ledger report (#47366)

(cherry picked from commit 3de249dcba)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js
#	erpnext/accounts/test/test_reports.py
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-05-05 18:18:23 +05:30
mergify[bot]
378821d9e6 fix: backward compatibility for renamed group_by filter on reports (backport #47362) (#47402)
fix: backward compatibility for renamed group_by filter on reports (#47362)

* fix: backward compatibility for renamed group_by filter in general ledger report

* fix: backward compatibility for renamed group_by filter in supplier quotation comparison report

(cherry picked from commit d4ffa54136)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-05-05 18:01:06 +05:30
ruthra kumar
698e260526 Merge pull request #47146 from frappe/mergify/copy/version-14-hotfix/pr-47145
refactor: make AR / AP report more memory efficient (copy #47145)
2025-05-05 15:43:33 +05:30
ruthra kumar
fc793f0f25 refactor: set default for fetch methods 2025-05-05 15:04:48 +05:30
ruthra kumar
204d1d6a53 refactor: use fetch method based on configuration 2025-05-05 15:04:48 +05:30
ruthra kumar
aee917b790 refactor: configurable fetch method for AR / AP report 2025-05-05 15:04:45 +05:30
ruthra kumar
892d3980d3 refactor: use unbuffered cursor for fetching 2025-05-05 14:59:24 +05:30
rohitwaghchaure
e743d5f66b chore: fix linters issue 2025-05-05 14:19:20 +05:30
rohitwaghchaure
9adb863787 chore: fix linters issue 2025-05-05 14:12:07 +05:30
Rohit Waghchaure
150cc5a664 fix: validation for difference account
(cherry picked from commit fb819c558e)
2025-05-03 07:52:53 +00:00
mergify[bot]
b507e63375 fix: completed transactions showing in the list (backport #47374) (#47378)
* fix: completed transactions showing in the list (#47374)

(cherry picked from commit 97db9da10e)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.js

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-02 15:29:10 +05:30
mergify[bot]
27a8856dca fix: renaming group by fieldname and value in reports (backport #47352) (#47359)
* fix: renaming group by fieldname and value in reports (#47352)

* fix: renaming in general ledger report

* fix: renaming in supplier quotation comparison report

* fix: renaming group by to categorize by in process statement of accounts

* fix: added patch

* fix: patch update to all documents

* chore: added patches to patch.txt

* chore: removing patch from v14

(cherry picked from commit 13a84e7f82)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
#	erpnext/accounts/report/general_ledger/general_ledger.py
#	erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py
#	erpnext/patches.txt
#	erpnext/patches/v14_0/rename_group_by_to_categorize_by.py

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

* chore: fixed path for patch file

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-04-30 18:22:59 +05:30
mergify[bot]
b13e0a6b9f fix: warning message before changing the valuation method (backport #47340) (#47341)
fix: warning message before changing the valuation method (#47340)

(cherry picked from commit ffdc4347e8)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-04-30 12:54:15 +05:30
41 changed files with 556 additions and 198 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.85.6"
__version__ = "14.85.9"
def get_default_company(user=None):

View File

@@ -73,9 +73,12 @@
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"ignore_is_opening_check_for_reporting",
"column_break_lvjk",
"receivable_payable_remarks_length"
"receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"legacy_section",
"ignore_is_opening_check_for_reporting"
],
"fields": [
{
@@ -479,6 +482,23 @@
"fieldname": "ignore_is_opening_check_for_reporting",
"fieldtype": "Check",
"label": "Ignore Is Opening check for reporting"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
"fieldtype": "Section Break",
"label": "Accounts Receivable / Payable Tuning"
},
{
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
}
],
"icon": "icon-cog",
@@ -486,7 +506,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-01-23 13:15:44.077853",
"modified": "2025-05-05 12:29:38.302027",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -515,4 +535,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
group_by: "Group by Voucher (Consolidated)",
categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -34,7 +34,7 @@ frappe.ui.form.on("Journal Entry", {
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
finance_book: frm.doc.finance_book,
group_by: "",
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -319,7 +319,7 @@ frappe.ui.form.on('Payment Entry', {
"from_date": frm.doc.posting_date,
"to_date": moment(frm.doc.modified).format('YYYY-MM-DD'),
"company": frm.doc.company,
"group_by": "",
"categorize_by": "",
"show_cancelled_entries": frm.doc.docstatus === 2
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -1590,7 +1590,7 @@ class PaymentEntry(AccountsController):
# Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references:
if not ref.payment_request:
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
@@ -1641,7 +1641,7 @@ class PaymentEntry(AccountsController):
)
# Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references:
if ref.payment_request:
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))

View File

@@ -29,7 +29,7 @@ frappe.ui.form.on("Period Closing Voucher", {
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
group_by: "",
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -32,8 +32,13 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self):
self.validate_future_closing_vouchers()
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
)
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},

View File

@@ -12,7 +12,7 @@
"posting_date",
"company",
"account",
"group_by",
"categorize_by",
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
@@ -172,14 +172,6 @@
"fieldtype": "Date",
"label": "Start Date"
},
{
"default": "Group by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "currency",
@@ -395,10 +387,18 @@
"fieldname": "show_remarks",
"fieldtype": "Check",
"label": "Show Remarks"
},
{
"default": "Categorize by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "categorize_by",
"fieldtype": "Select",
"label": "Categorize By",
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
}
],
"links": [],
"modified": "2024-10-18 17:51:39.108481",
"modified": "2025-04-30 14:43:23.643006",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
@@ -433,4 +433,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -145,7 +145,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
"party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"categorize_by": doc.categorize_by,
"currency": doc.currency,
"project": [p.project_name for p in doc.project],
"show_opening_entries": 0,

View File

@@ -597,35 +597,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
def validate_due_date(
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None, doctype=None
):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:
if not template_name:
return
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
"%Y-%m-%d"
)
if not default_due_date:
return
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
if not template_name:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
is_credit_controller = (
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
if not default_due_date:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
msgprint(
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
party_type, date_diff(due_date, default_due_date)
)
)
if is_credit_controller:
msgprint(
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
date_diff(due_date, default_due_date)
)
)
else:
frappe.throw(
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
)
else:
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
@frappe.whitelist()
@@ -903,12 +902,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
["is_shipping_address", "=", 1],
["address_type", "=", "Shipping"],
],
pluck="name",
limit=1,
fields=["name", "is_shipping_address"],
order_by="is_shipping_address DESC",
)
return shipping_addresses[0] if shipping_addresses else None
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
return shipping_addresses[0].name
if len(shipping_addresses) == 1:
return shipping_addresses[0].name
else:
return None
def get_partywise_advanced_payment_amount(

View File

@@ -49,6 +49,10 @@ class ReceivablePayableReport:
self.age_as_on = (
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
)
self.ple_fetch_method = (
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor"
) # Fail Safe
def run(self, args):
self.filters.update(args)
@@ -85,10 +89,7 @@ class ReceivablePayableReport:
self.skip_total_row = 1
def get_data(self):
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
@@ -105,12 +106,40 @@ class ReceivablePayableReport:
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.prepare_ple_query()
self.data = []
self.voucher_balance = OrderedDict()
if self.ple_fetch_method == "Buffered Cursor":
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
self.build_data()
def fetch_ple_in_buffered_cursor(self):
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries:
self.update_voucher_balance(ple)
self.build_data()
delattr(self, "ple_entries")
def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = []
with frappe.db.unbuffered_cursor():
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple)
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries:
self.update_voucher_balance(ple)
delattr(self, "ple_entries")
def build_voucher_dict(self, ple):
return frappe._dict(
@@ -131,26 +160,22 @@ class ReceivablePayableReport:
outstanding_in_account_currency=0.0,
)
def init_voucher_balance(self):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
def init_voucher_balance(self, ple):
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
self.voucher_balance[key].cost_center = ple.cost_center
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
self.voucher_balance[key].cost_center = ple.cost_center
self.get_invoices(ple)
self.get_invoices(ple)
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
self.init_subtotal_row("Total")
@@ -764,7 +789,7 @@ class ReceivablePayableReport:
index = 4
row["range" + str(index + 1)] = row.outstanding
def get_ple_entries(self):
def prepare_ple_query(self):
# get all the GL entries filtered by the given filters
self.prepare_conditions()
@@ -817,7 +842,7 @@ class ReceivablePayableReport:
else:
query = query.orderby(self.ple.posting_date, self.ple.party)
self.ple_entries = query.run(as_dict=True)
self.ple_query = query
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):

View File

@@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = {
label: __("Voucher No"),
fieldtype: "Data",
on_change: function () {
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
},
},
{
@@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
hidden: 1,
},
{
fieldname: "group_by",
label: __("Group by"),
fieldname: "categorize_by",
label: __("Categorize by"),
fieldtype: "Select",
options: [
"",
{
label: __("Group by Voucher"),
value: "Group by Voucher",
label: __("Categorize by Voucher"),
value: "Categorize by Voucher",
},
{
label: __("Group by Voucher (Consolidated)"),
value: "Group by Voucher (Consolidated)",
label: __("Categorize by Voucher (Consolidated)"),
value: "Categorize by Voucher (Consolidated)",
},
{
label: __("Group by Account"),
value: "Group by Account",
label: __("Categorize by Account"),
value: "Categorize by Account",
},
{
label: __("Group by Party"),
value: "Group by Party",
label: __("Categorize by Party"),
value: "Categorize by Party",
},
],
default: "Group by Voucher (Consolidated)",
default: "Categorize by Voucher (Consolidated)",
},
{
fieldname: "tax_id",

View File

@@ -72,13 +72,17 @@ def validate_filters(filters, account_details):
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if filters.get("account") and filters.get("group_by") == "Group by Account":
if not filters.get("categorize_by") and filters.get("group_by"):
filters["categorize_by"] = filters["group_by"]
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
filters.account = frappe.parse_json(filters.get("account"))
for account in filters.account:
if account_details[account].is_group == 0:
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
if filters.from_date > filters.to_date:
@@ -172,9 +176,9 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("include_dimensions"):
order_by_statement = "order by posting_date, creation"
if filters.get("group_by") == "Group by Voucher":
if filters.get("categorize_by") == "Categorize by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("group_by") == "Group by Account":
if filters.get("categorize_by") == "Categorize by Account":
order_by_statement = "order by account, posting_date, creation"
if filters.get("include_default_book_entries"):
@@ -261,7 +265,7 @@ def get_conditions(filters):
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')")
if filters.get("party_type"):
@@ -273,7 +277,7 @@ def get_conditions(filters):
if not (
filters.get("account")
or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"]
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
):
if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
@@ -368,13 +372,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
data.append(totals.opening)
if filters.get("group_by") != "Group by Voucher (Consolidated)":
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
for _acc, acc_dict in gle_map.items():
# acc
if acc_dict.entries:
# opening
data.append({})
if filters.get("group_by") != "Group by Voucher":
if filters.get("categorize_by") != "Categorize by Voucher":
data.append(acc_dict.totals.opening)
data += acc_dict.entries
@@ -383,7 +387,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
data.append(acc_dict.totals.total)
# closing
if filters.get("group_by") != "Group by Voucher":
if filters.get("categorize_by") != "Categorize by Voucher":
data.append(acc_dict.totals.closing)
data.append({})
else:
@@ -416,9 +420,9 @@ def get_totals_dict():
def group_by_field(group_by):
if group_by == "Group by Party":
if group_by == "Categorize by Party":
return "party"
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
return "account"
else:
return "voucher_no"
@@ -426,7 +430,7 @@ def group_by_field(group_by):
def initialize_gle_map(gl_entries, filters):
gle_map = OrderedDict()
group_by = group_by_field(filters.get("group_by"))
group_by = group_by_field(filters.get("categorize_by"))
for gle in gl_entries:
gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[]))
@@ -437,8 +441,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
totals = get_totals_dict()
entries = []
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get("group_by"))
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
group_by = group_by_field(filters.get("categorize_by"))
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
if filters.get("show_net_values_in_party_account"):
account_type_map = get_account_type_map(filters.get("company"))
@@ -534,17 +538,20 @@ def get_account_type_map(company):
def get_result_as_list(data, filters):
balance, _balance_in_account_currency = 0, 0
balance = 0
for d in data:
if not d.get("posting_date"):
balance, _balance_in_account_currency = 0, 0
balance = 0
balance = get_balance(d, balance, "debit", "credit")
d["balance"] = balance
d["account_currency"] = filters.account_currency
d["presentation_currency"] = filters.presentation_currency
return data
@@ -570,11 +577,8 @@ def get_columns(filters):
if filters.get("presentation_currency"):
currency = filters["presentation_currency"]
else:
if filters.get("company"):
currency = get_company_currency(filters["company"])
else:
company = get_default_company()
currency = get_company_currency(company)
company = filters.get("company") or get_default_company()
filters["presentation_currency"] = currency = get_company_currency(company)
columns = [
{
@@ -595,19 +599,22 @@ def get_columns(filters):
{
"label": _("Debit ({0})").format(currency),
"fieldname": "debit",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
{
"label": _("Credit ({0})").format(currency),
"fieldname": "credit",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
{
"label": _("Balance ({0})").format(currency),
"fieldname": "balance",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},

View File

@@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"categorize_by": "Categorize by Voucher (Consolidated)",
}
)
)
@@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_err": True,
}
)
@@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_err": False,
}
)
@@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": si.posting_date,
"to_date": si.posting_date,
"account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)",
"categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_cr_dr_notes": False,
}
)
@@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": si.posting_date,
"to_date": si.posting_date,
"account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)",
"categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_cr_dr_notes": True,
}
)

View File

@@ -12,8 +12,8 @@ DEFAULT_FILTERS = {
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}),
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}),
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Consolidated Financial Statement", {"report": "Balance Sheet"}),

View File

@@ -152,6 +152,7 @@
{
"allow_on_submit": 1,
"fetch_from": "item_code.image",
"fetch_if_empty": 1,
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
@@ -582,7 +583,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-01-15 17:35:49.226603",
"modified": "2025-05-20 00:44:06.229177",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
},
},
{
fieldname: "group_by",
label: __("Group by"),
fieldname: "categorize_by",
label: __("Categorize by"),
fieldtype: "Select",
options: [
{ label: __("Group by Supplier"), value: "Group by Supplier" },
{ label: __("Group by Item"), value: "Group by Item" },
{ label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
{ label: __("Categorize by Item"), value: "Categorize by Item" },
],
default: __("Group by Supplier"),
default: __("Categorize by Supplier"),
},
{
fieldtype: "Check",

View File

@@ -15,6 +15,8 @@ def execute(filters=None):
if not filters:
return [], []
validate_filters(filters)
columns = get_columns(filters)
supplier_quotation_data = get_data(filters)
@@ -24,6 +26,12 @@ def execute(filters=None):
return columns, data, message, chart_data
def validate_filters(filters):
if not filters.get("categorize_by") and filters.get("group_by"):
filters["categorize_by"] = filters["group_by"]
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
def get_data(filters):
sq = frappe.qb.DocType("Supplier Quotation")
sq_item = frappe.qb.DocType("Supplier Quotation Item")
@@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters):
group_wise_map = defaultdict(list)
supplier_qty_price_map = {}
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
group_by_field = (
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
)
company_currency = frappe.db.get_default("currency")
float_precision = cint(frappe.db.get_default("float_precision")) or 2
@@ -274,7 +284,7 @@ def get_columns(filters):
},
]
if filters.get("group_by") == "Group by Item":
if filters.get("categorize_by") == "Categorize by Item":
group_by_columns.reverse()
columns[0:0] = group_by_columns # add positioned group by columns to the report

View File

@@ -684,7 +684,9 @@ class AccountsController(TransactionBase):
"Customer",
self.customer,
self.company,
None,
self.payment_terms_template,
self.doctype,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
@@ -695,6 +697,7 @@ class AccountsController(TransactionBase):
self.company,
self.bill_date,
self.payment_terms_template,
self.doctype,
)
def set_price_list_currency(self, buying_or_selling):

View File

@@ -368,7 +368,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value("Delivery Note", source_name, "company")
company = frappe.db.get_value(doctype, source_name, "company")
default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return"
)

View File

@@ -7,7 +7,7 @@ from collections import deque
from operator import itemgetter
import frappe
from frappe import _
from frappe import _, bold
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, today
@@ -554,9 +554,16 @@ class BOM(WebsiteGenerator):
def check_recursion(self, bom_list=None):
"""Check whether recursion occurs in any bom"""
def _throw_error(bom_name):
def _throw_error(bom_name, production_item=None):
msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name)
if production_item and bom_name != self.name:
msg += "<br><br>"
msg += _(
"Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material."
).format(bold(production_item))
frappe.throw(
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
msg,
exc=BOMRecursionError,
)
@@ -573,7 +580,7 @@ class BOM(WebsiteGenerator):
if self.item == item.item_code and item.bom_no:
# Same item but with different BOM should not be allowed.
# Same item can appear recursively once as long as it doesn't have BOM.
_throw_error(item.bom_no)
_throw_error(item.bom_no, self.item)
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)

View File

@@ -372,3 +372,6 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v14_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on

View File

@@ -0,0 +1,20 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
rename_field("Process Statement Of Accounts", "group_by", "categorize_by")
frappe.db.sql(
"""
UPDATE
`tabProcess Statement Of Accounts`
SET
categorize_by = CASE
WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)'
WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher'
END
WHERE
categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher')
"""
)

View File

@@ -0,0 +1,14 @@
import frappe
from frappe.utils import cint
def execute():
frappe.db.set_single_value(
"Stock Settings",
"update_price_list_based_on",
(
"Price List Rate"
if cint(frappe.db.get_single_value("Selling Settings", "editable_price_list_rate"))
else "Rate"
),
)

View File

@@ -87,7 +87,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
group_by: "Group by Voucher (Consolidated)",
categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true
};

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
frm.add_custom_button(
__("Add Sales Partners"),
function () {
new frappe.desk.eventParticipants(frm, "Sales Partners");
new frappe.desk.eventParticipants(frm, "Sales Partner");
},
__("Add Participants")
);

View File

@@ -853,7 +853,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
def test_auto_insert_price(self):
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
frappe.db.set_single_value(
"Stock Settings",
{
"auto_insert_price_list_rate_if_missing": 1,
"update_price_list_based_on": "Price List Rate",
},
)
item_price = frappe.db.get_value(
"Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}
@@ -865,6 +871,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100
)
# ensure price gets inserted based on rate if price list rate is not defined by user
self.assertEqual(
frappe.db.get_value(
"Item Price",
@@ -874,6 +881,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
100,
)
# ensure price gets insterted based on user-defined *Price List Rate*
# if update_price_list_based_on is set to Price List Rate
make_sales_order(
item_code="_Test Item for Auto Price List with Discount Percentage",
selling_price_list="_Test Price List",
@@ -881,18 +890,43 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
discount_percentage=20,
)
self.assertEqual(
frappe.db.get_value(
"Item Price",
{
"price_list": "_Test Price List",
"item_code": "_Test Item for Auto Price List with Discount Percentage",
},
"price_list_rate",
),
200,
item_price = frappe.db.get_value(
"Item Price",
{
"price_list": "_Test Price List",
"item_code": "_Test Item for Auto Price List with Discount Percentage",
},
("name", "price_list_rate"),
as_dict=True,
)
self.assertEqual(item_price.price_list_rate, 200)
frappe.delete_doc("Item Price", item_price.name)
frappe.db.set_single_value("Stock Settings", "update_price_list_based_on", "Rate")
# ensure price gets insterted based on user-defined *Rate*
# if update_price_list_based_on is set to Rate
make_sales_order(
item_code="_Test Item for Auto Price List with Discount Percentage",
selling_price_list="_Test Price List",
price_list_rate=200,
discount_percentage=20,
)
item_price = frappe.db.get_value(
"Item Price",
{
"price_list": "_Test Price List",
"item_code": "_Test Item for Auto Price List with Discount Percentage",
},
("name", "price_list_rate"),
as_dict=True,
)
self.assertEqual(item_price.price_list_rate, 160)
frappe.delete_doc("Item Price", item_price.name)
# do not update price list
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
@@ -917,6 +951,63 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
def test_update_existing_item_price(self):
item_code = "_Test Item for Price List Updation"
price_list = "_Test Price List"
make_item(item_code, {"is_stock_item": 0})
frappe.db.set_single_value(
"Stock Settings",
{
"auto_insert_price_list_rate_if_missing": 1,
"update_existing_price_list_rate": 1,
"update_price_list_based_on": "Rate",
},
)
# setup: price creation
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=100)
# test price updation based on Rate
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=90)
self.assertEqual(
frappe.db.get_value(
"Item Price",
{"price_list": price_list, "item_code": item_code},
"price_list_rate",
),
90,
)
frappe.db.set_single_value(
"Stock Settings",
{
"update_price_list_based_on": "Price List Rate",
},
)
# test price updation based on Price List Rate
make_sales_order(
item_code=item_code,
selling_price_list=price_list,
price_list_rate=200,
discount_percentage=20,
)
self.assertEqual(
frappe.db.get_value(
"Item Price",
{"price_list": price_list, "item_code": item_code},
"price_list_rate",
),
200,
)
# reset `update_existing_price_list_rate` to 0
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
def test_drop_shipping(self):
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
from erpnext.selling.doctype.sales_order.sales_order import (

View File

@@ -2,5 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("Selling Settings", {
refresh: function (frm) {},
after_save(frm) {
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
},
});

View File

@@ -34,6 +34,7 @@ def set_default_settings(args):
stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.update_price_list_based_on = "Rate"
stock_settings.automatically_set_serial_nos_based_on_fifo = 1
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save()

View File

@@ -478,6 +478,7 @@ def update_stock_settings():
stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.update_price_list_based_on = "Rate"
stock_settings.automatically_set_serial_nos_based_on_fifo = 1
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save()

View File

@@ -207,13 +207,12 @@ def get_or_create_account(company_name, account):
default_root_type = "Liability"
root_type = account.get("root_type", default_root_type)
or_filters = {"account_name": account.get("account_name")}
if account.get("account_number"):
or_filters.update({"account_number": account.get("account_number")})
existing_accounts = frappe.get_all(
"Account",
filters={"company": company_name, "root_type": root_type},
or_filters={
"account_name": account.get("account_name"),
"account_number": account.get("account_number"),
},
"Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters
)
if existing_accounts:

View File

@@ -491,16 +491,20 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
from frappe.query_builder.functions import Sum
# Billed against Sales Order directly
si = frappe.qb.DocType("Sales Invoice").as_("si")
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
sum_amount = Sum(si_item.amount).as_("amount")
billed_against_so = (
frappe.qb.from_(si_item)
.join(si)
.on(si.name == si_item.parent)
.select(sum_amount)
.where(
(si_item.so_detail == so_detail)
& ((si_item.dn_detail.isnull()) | (si_item.dn_detail == ""))
& (si_item.docstatus == 1)
& (si.update_stock == 0)
)
.run()
)

View File

@@ -865,6 +865,28 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.status, "Completed")
def test_dn_billing_status_case5(self):
# SO -> SI(with update stock partial invoice)
# SO -> DN
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
so = make_sales_order(po_no="12345")
si = make_sales_invoice(so.name)
si.get("items")[0].qty = 5
si.update_stock = 1
si.submit()
# Testing if Customer's Purchase Order No was rightly copied
self.assertEqual(so.po_no, si.po_no)
dn = make_delivery_note(so.name)
dn.submit()
self.assertEqual(dn.get("items")[0].billed_amt, 0)
self.assertEqual(dn.per_billed, 0)
self.assertEqual(dn.status, "To Bill")
def test_delivery_trip(self):
dn = create_delivery_note()
dt = make_delivery_trip(dn.name)

View File

@@ -7,6 +7,35 @@ const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invo
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
frappe.ui.form.on("Item", {
valuation_method(frm) {
if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") {
let stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
let current_valuation_method = frm.doc.__onload.current_valuation_method;
if (stock_exists && current_valuation_method !== frm.doc.valuation_method) {
let msg = __(
"Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances."
);
msg += "<br>";
msg += __(
"Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item."
);
msg += "<br>";
msg += __("Do you want to change valuation method?");
frappe.confirm(
msg,
() => {
frm.set_value("valuation_method", "Moving Average");
},
() => {
frm.set_value("valuation_method", current_valuation_method);
}
);
}
}
},
setup: function (frm) {
frm.add_fetch("attribute", "numeric_values", "numeric_values");
frm.add_fetch("attribute", "from_range", "from_range");

View File

@@ -31,6 +31,7 @@ from erpnext.controllers.item_variant import (
)
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
from erpnext.stock.doctype.item_default.item_default import ItemDefault
from erpnext.stock.utils import get_valuation_method
class DuplicateReorderRows(frappe.ValidationError):
@@ -53,6 +54,7 @@ class Item(Document):
def onload(self):
self.set_onload("stock_exists", self.stock_ledger_created())
self.set_onload("asset_naming_series", get_asset_naming_series())
self.set_onload("current_valuation_method", get_valuation_method(self.name))
def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series":

View File

@@ -364,6 +364,7 @@ frappe.ui.form.on('Stock Entry', {
docstatus: 1,
purpose: "Material Transfer",
add_to_transit: 1,
per_transferred: ["<", 100],
}
})
}, __("Get Items From"));

View File

@@ -6,10 +6,20 @@ import json
from collections import defaultdict
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
from frappe.utils import (
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
get_link_to_form,
getdate,
nowdate,
)
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -430,17 +440,37 @@ class StockEntry(StockController):
).format(frappe.bold(self.company))
)
elif (
self.is_opening == "Yes"
and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss"
):
acc_details = frappe.get_cached_value(
"Account",
d.expense_account,
["account_type", "report_type"],
as_dict=True,
)
if self.is_opening == "Yes" and acc_details.report_type == "Profit and Loss":
frappe.throw(
_(
"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"
"Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry"
),
OpeningEntryAccountError,
)
if acc_details.account_type == "Stock":
frappe.throw(
_(
"At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
).format(d.idx, get_link_to_form("Account", d.expense_account)),
OpeningEntryAccountError,
)
if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold":
frappe.msgprint(
_(
"At row {0}: You have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account"
).format(d.idx, bold(get_link_to_form("Account", d.expense_account))),
title=_("Warning : Cost of Goods Sold Account"),
)
def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse"""

View File

@@ -35,4 +35,30 @@ frappe.ui.form.on("Stock Settings", {
}
);
},
auto_insert_price_list_rate_if_missing(frm) {
if (!frm.doc.auto_insert_price_list_rate_if_missing) return;
frm.set_value(
"update_price_list_based_on",
cint(frappe.defaults.get_default("editable_price_list_rate")) ? "Price List Rate" : "Rate"
);
},
update_price_list_based_on(frm) {
if (
frm.doc.update_price_list_based_on === "Price List Rate" &&
!cint(frappe.defaults.get_default("editable_price_list_rate"))
) {
const dialog = frappe.warn(
__("Incompatible Setting Detected"),
__(
"<p>Price List Rate has not been set as editable in Selling Settings. In this scenario, setting <strong>Update Price List Based On</strong> to <strong>Price List Rate</strong> will prevent auto-updation of Item Price.</p>Are you sure you want to continue?"
)
);
dialog.set_secondary_action(() => {
frm.set_value("update_price_list_based_on", "Rate");
dialog.hide();
});
return;
}
},
});

View File

@@ -16,6 +16,7 @@
"stock_uom",
"price_list_defaults_section",
"auto_insert_price_list_rate_if_missing",
"update_price_list_based_on",
"column_break_12",
"update_existing_price_list_rate",
"stock_validations_tab",
@@ -347,6 +348,15 @@
"fieldname": "allow_existing_serial_no",
"fieldtype": "Check",
"label": "Allow existing Serial No to be Manufactured/Received again"
},
{
"default": "Rate",
"depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"fieldname": "update_price_list_based_on",
"fieldtype": "Select",
"label": "Update Price List Based On",
"mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"options": "Rate\nPrice List Rate"
}
],
"icon": "icon-cog",
@@ -354,7 +364,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-05-31 14:15:14.145048",
"modified": "2025-05-06 02:39:24.284587",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -855,8 +855,8 @@ def get_price_list_rate(args, item_doc, out=None):
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database
if price_list_rate is None or frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate"
if price_list_rate is None or frappe.get_cached_value(
"Stock Settings", "Stock Settings", "update_existing_price_list_rate"
):
insert_item_price(args)
@@ -890,49 +890,71 @@ def insert_item_price(args):
):
return
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
):
if frappe.has_permission("Item Price", "write"):
price_list_rate = (
(flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor")
else (flt(args.rate) + flt(args.discount_amount))
)
stock_settings = frappe.get_cached_doc("Stock Settings")
item_price = frappe.db.get_value(
"Item Price",
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
["name", "price_list_rate"],
as_dict=1,
)
if item_price and item_price.name:
if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate"
):
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
frappe.msgprint(
_("Item Price updated for {0} in Price List {1}").format(
args.item_code, args.price_list
),
alert=True,
)
else:
item_price = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": args.price_list,
"item_code": args.item_code,
"currency": args.currency,
"price_list_rate": price_list_rate,
"uom": args.stock_uom,
}
)
item_price.insert()
frappe.msgprint(
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list),
alert=True,
)
if (
not frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency
or not stock_settings.auto_insert_price_list_rate_if_missing
or not frappe.has_permission("Item Price", "write")
):
return
item_price = frappe.db.get_value(
"Item Price",
{
"item_code": args.item_code,
"price_list": args.price_list,
"currency": args.currency,
"uom": args.stock_uom,
},
["name", "price_list_rate"],
as_dict=1,
)
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
if item_price and item_price.name:
if not stock_settings.update_existing_price_list_rate:
return
rate_to_consider = flt(args.price_list_rate) if update_based_on_price_list_rate else flt(args.rate)
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
if not price_list_rate or item_price.price_list_rate == price_list_rate:
return
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
frappe.msgprint(
_("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list),
alert=True,
)
else:
rate_to_consider = (
(flt(args.price_list_rate) or flt(args.rate))
if update_based_on_price_list_rate
else flt(args.rate)
)
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
item_price = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": args.price_list,
"item_code": args.item_code,
"currency": args.currency,
"price_list_rate": price_list_rate,
"uom": args.stock_uom,
}
)
item_price.insert()
frappe.msgprint(
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list),
alert=True,
)
def _get_stock_uom_rate(rate, args):
return rate / args.conversion_factor if args.conversion_factor else rate
def get_item_price(args, item_code, ignore_party=False):