Compare commits

..

77 Commits

Author SHA1 Message Date
Frappe PR Bot
b6575176f7 chore(release): Bumped to Version 14.65.4
## [14.65.4](https://github.com/frappe/erpnext/compare/v14.65.3...v14.65.4) (2024-03-20)

### Bug Fixes

* added index for voucher_detail_no in gl entry ([a1c8b93](a1c8b93e99))
* currency symbol for Landed Cost Voucher Amount (backport [#40550](https://github.com/frappe/erpnext/issues/40550)) ([#40553](https://github.com/frappe/erpnext/issues/40553)) ([7851311](785131121d))
* Data too long for column 'stock_queue' ([#40450](https://github.com/frappe/erpnext/issues/40450)) ([3340463](3340463b8e))
* flaky ecommerce test case ([#40534](https://github.com/frappe/erpnext/issues/40534)) ([b197c19](b197c1926f))
* get unique provisional accounts ([39d958c](39d958c507))
* index error on Exchange Rate Revaluation creation ([8813b6b](8813b6b2fd))
* invalid exchange loss booking on invoice againts base accounts ([a27f386](a27f386665))
* missing range for ageing summary ([9f0ad7a](9f0ad7a4ea))
* provisional entry for non-stock item ([0aef7f9](0aef7f9671))
* **Supplier Quotation Comparison:** group by options ([cfed706](cfed706c15))
* Update Existing Price List Rate not working (backport [#40333](https://github.com/frappe/erpnext/issues/40333)) ([#40526](https://github.com/frappe/erpnext/issues/40526)) ([64f1844](64f1844bc8))
* warehouse issue in pick list ([#40453](https://github.com/frappe/erpnext/issues/40453)) ([5a2eaf9](5a2eaf9092))
2024-03-20 05:23:36 +00:00
rohitwaghchaure
edce5770f0 Merge pull request #40537 from frappe/version-14-hotfix
chore: release v14
2024-03-20 10:52:20 +05:30
mergify[bot]
785131121d fix: currency symbol for Landed Cost Voucher Amount (backport #40550) (#40553)
* fix: currency symbol for Landed Cost Voucher Amount (#40550)

(cherry picked from commit 0725707cb1)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-19 23:37:32 +05:30
Nabin Hait
419f2a5192 Merge pull request #40519 from nabinhait/provisional-entry-fix
fix: provisional entry for non-stock item
2024-03-19 21:58:56 +05:30
Nabin Hait
39d958c507 fix: get unique provisional accounts 2024-03-19 21:06:16 +05:30
Nabin Hait
a1c8b93e99 fix: added index for voucher_detail_no in gl entry 2024-03-19 18:35:11 +05:30
Nabin Hait
0aef7f9671 fix: provisional entry for non-stock item 2024-03-19 18:29:37 +05:30
mergify[bot]
64f1844bc8 fix: Update Existing Price List Rate not working (backport #40333) (#40526)
* fix: Update Existing Price List Rate not working (#40333)

(cherry picked from commit 09ea7edb86)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
#	erpnext/setup/demo_data/item.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-19 18:11:50 +05:30
ruthra kumar
b197c1926f fix: flaky ecommerce test case (#40534)
* refactor(test): make use of fixtures

* refactor(test): rating are now restricted with [0..1]

* refactor(test): remove superfluous setup override
2024-03-19 17:54:35 +05:30
Raffael Meyer
76f27d99bb Merge pull request #40547 from frappe/mergify/bp/version-14-hotfix/pr-40546
fix(Supplier Quotation Comparison): group by options (backport #40546)
2024-03-19 13:07:40 +01:00
barredterra
cfed706c15 fix(Supplier Quotation Comparison): group by options
(cherry picked from commit 87e36d290e)
2024-03-19 11:51:37 +00:00
Gursheen Kaur Anand
93fe136386 Merge pull request #40530 from frappe/mergify/bp/version-14-hotfix/pr-40528
fix(minor): missing range for ageing summary in PSOA (backport #40528)
2024-03-19 12:44:16 +05:30
Gursheen Anand
9f0ad7a4ea fix: missing range for ageing summary
(cherry picked from commit 643cc022fd)
2024-03-19 07:11:18 +00:00
ruthra kumar
59a9a41e9d Merge pull request #40522 from frappe/mergify/bp/version-14-hotfix/pr-40502
refactor: toggle between 'http' and 'https' on exchange rate API (backport #40502)
2024-03-19 10:37:32 +05:30
ruthra kumar
33efa6960d chore: resolve conflicts 2024-03-19 10:14:24 +05:30
ruthra kumar
379616ab4b refactor: toggle between 'http' and 'https' on exchange rate API
(cherry picked from commit 8b81274769)

# Conflicts:
#	erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
2024-03-19 04:29:44 +00:00
ruthra kumar
8cb1293ffc Merge pull request #40515 from frappe/mergify/bp/version-14-hotfix/pr-40511
fix: invalid exchange loss booking on invoice against base accounts (backport #40511)
2024-03-18 21:05:51 +05:30
ruthra kumar
fe78214ea4 refactor(test): ensure Exchange gain/loss journals aren't created
(cherry picked from commit e1c2d006ca)
2024-03-18 15:14:44 +00:00
ruthra kumar
a27f386665 fix: invalid exchange loss booking on invoice againts base accounts
(cherry picked from commit 3d5dba6976)
2024-03-18 15:14:44 +00:00
ruthra kumar
c4c0510a1b Merge pull request #40496 from frappe/mergify/bp/version-14-hotfix/pr-40495
fix: index error on Exchange Rate Revaluation creation (backport #40495)
2024-03-17 08:10:46 +05:30
ruthra kumar
8813b6b2fd fix: index error on Exchange Rate Revaluation creation
(cherry picked from commit bb279e368c)
2024-03-17 02:21:33 +00:00
ruthra kumar
a0e839e31a Merge pull request #40489 from frappe/mergify/bp/version-14-hotfix/pr-40485
refactor: validate SO and SI references in Delivery Notes (backport #40485)
2024-03-16 15:28:12 +05:30
ruthra kumar
9d827e84be chore: resolve conflicts 2024-03-16 15:02:23 +05:30
ruthra kumar
f87f284616 test: SO reference validation
(cherry picked from commit 4f396d3049)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
2024-03-16 09:29:07 +00:00
ruthra kumar
5f4a648dd4 refactor: validate SO and SI references
(cherry picked from commit 4d090bd3b8)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.py
2024-03-16 09:29:07 +00:00
ruthra kumar
5f0920ab27 Merge pull request #40475 from frappe/mergify/bp/version-14-hotfix/pr-40473
refactor: enable no-copy for update_outstanding_for_self (backport #40473)
2024-03-16 10:09:37 +05:30
ruthra kumar
03cb07f591 Merge pull request #40465 from frappe/mergify/bp/version-14-hotfix/pr-40461
refactor: disallow '0' qty return invoices with stock effect (backport #40461)
2024-03-16 10:07:31 +05:30
ruthra kumar
90a5155fc1 refactor: enable no-copy for update_outstanding_for_self
(cherry picked from commit a1e8caa5c1)
2024-03-15 11:33:09 +00:00
ruthra kumar
0437274c58 chore: resolve conflict 2024-03-15 11:39:54 +05:30
ruthra kumar
a914eb5d40 test: validation to prevent '0' qty return with stock effect
(cherry picked from commit 647bba0f00)
2024-03-15 05:37:23 +00:00
ruthra kumar
b011a68a2a refactor: disallow '0' qty return invoices with stock effect
(cherry picked from commit 898affbee9)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2024-03-15 05:37:22 +00:00
Frappe PR Bot
009cdb732f chore(release): Bumped to Version 14.65.3
## [14.65.3](https://github.com/frappe/erpnext/compare/v14.65.2...v14.65.3) (2024-03-14)

### Bug Fixes

* Data too long for column 'stock_queue' (backport [#40450](https://github.com/frappe/erpnext/issues/40450)) ([#40452](https://github.com/frappe/erpnext/issues/40452)) ([0999d09](0999d09700))
* warehouse issue in pick list (backport [#40453](https://github.com/frappe/erpnext/issues/40453)) ([#40455](https://github.com/frappe/erpnext/issues/40455)) ([314e9f5](314e9f5787))
2024-03-14 07:36:22 +00:00
mergify[bot]
314e9f5787 fix: warehouse issue in pick list (backport #40453) (#40455)
fix: warehouse issue in pick list (#40453)

(cherry picked from commit 5a2eaf9092)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-14 13:04:41 +05:30
mergify[bot]
0999d09700 fix: Data too long for column 'stock_queue' (backport #40450) (#40452)
fix: Data too long for column 'stock_queue' (#40450)

(cherry picked from commit 3340463b8e)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-14 13:04:16 +05:30
rohitwaghchaure
5a2eaf9092 fix: warehouse issue in pick list (#40453) 2024-03-14 12:17:23 +05:30
rohitwaghchaure
3340463b8e fix: Data too long for column 'stock_queue' (#40450) 2024-03-14 11:39:29 +05:30
Frappe PR Bot
561f6a7af7 chore(release): Bumped to Version 14.65.2
## [14.65.2](https://github.com/frappe/erpnext/compare/v14.65.1...v14.65.2) (2024-03-14)

### Bug Fixes

* added index for price_list column in Item Price ([d279e23](d279e23623))
* Book depreciation until the asset disposal date and removed unwanted commits ([5fcf8c5](5fcf8c588a))
* convert str to date ([34c3e75](34c3e75d5c))
* flaky Accounts Receivable test case ([41bda58](41bda583e8))
* incorrect gross profit on the quotation (backport [#40438](https://github.com/frappe/erpnext/issues/40438)) ([#40441](https://github.com/frappe/erpnext/issues/40441)) ([fe54590](fe54590af7))
* linter issues ([ac0dd50](ac0dd5083f))
* linter issues ([668a931](668a93128b))
* linter issues ([acc0b2f](acc0b2faf8))
* minor fixes ([5cd9bf3](5cd9bf3bda))
* Patch to remove cancelled asset capitalization from asset ([23e256a](23e256aedf))
* Reload document on cancel to avoid document modified error ([c7ed854](c7ed854850))
* **test:** manually filter rows and assert ([5fe200b](5fe200bc4d))

### Performance Improvements

* Cache accounting dimension filter map ([e4bd173](e4bd173875))
* Cached accounting dimensions details ([8cd8b8f](8cd8b8f885))
* Caching in checking allowance for qty and amount ([8d682fa](8d682fa884))
* Caching in gl entry ([b07769d](b07769d8d7))
* Get bin details only for stock items ([6ff9e6e](6ff9e6ee84))
* Optimization for providional gl entries ([d7b738f](d7b738ff61))
* Optimzed code for merging similar gl entries ([aa75a60](aa75a60142))
* Performance optimization for validating budget ([f204d81](f204d810bb))
* refactored handling provisional gl entries for non-stock items ([49c7436](49c74369a5))
* skip unnecessary validation while transaction  cancellation ([05385e4](05385e4acb))
* validate expense against budget only if budget exists ([c15b2d5](c15b2d5490))
2024-03-14 06:01:38 +00:00
Nabin Hait
aa7c00135f Merge pull request #40443 from frappe/version-14-hotfix
chore: release v14
2024-03-14 11:30:17 +05:30
ruthra kumar
8500f9dad1 Merge pull request #40446 from frappe/mergify/copy/version-14-hotfix/pr-40445
fix: flaky Accounts Receivable test case (copy #40445)
2024-03-14 09:16:33 +05:30
ruthra kumar
5fe200bc4d fix(test): manually filter rows and assert 2024-03-14 08:54:41 +05:30
ruthra kumar
41bda583e8 fix: flaky Accounts Receivable test case
(cherry picked from commit 40bce240bde508801d07b9c38ededea5b4f297c8)
2024-03-13 16:33:46 +00:00
mergify[bot]
fe54590af7 fix: incorrect gross profit on the quotation (backport #40438) (#40441)
fix: incorrect gross profit on the quotation (#40438)

(cherry picked from commit f4a945aee4)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-13 21:55:26 +05:30
Nabin Hait
fa55cc68f8 Merge pull request #40263 from nabinhait/pi-optimization
perf: Performance optmization for Purchase Invoice submission
2024-03-13 18:17:41 +05:30
ruthra kumar
fa0a60c5d9 Merge pull request #40426 from frappe/mergify/bp/version-14-hotfix/pr-40372
refactor: checkbox to toggle standalone Credit/Debit note behaviour (backport #40372)
2024-03-13 16:28:38 +05:30
Nabin Hait
c1428a5655 Merge pull request #40246 from nabinhait/depreciation-asset-sale
fix: Book depreciation until the asset disposal date and removed unwanted commits
2024-03-13 13:17:18 +05:30
Nabin Hait
43a6a00912 Merge pull request #40125 from nabinhait/document-modified-error
fix: Reload document on cancel to avoid document modified error
2024-03-13 13:17:05 +05:30
ruthra kumar
5d178d2e24 refactor: pass 'name' to get_link_to_form 2024-03-13 13:16:56 +05:30
Nabin Hait
b5f0576a1b Merge pull request #40122 from nabinhait/remove-capitalized-in
fix: Patch to remove cancelled asset capitalization from asset
2024-03-13 13:16:29 +05:30
Nabin Hait
b07769d8d7 perf: Caching in gl entry 2024-03-13 13:02:51 +05:30
Nabin Hait
8d682fa884 perf: Caching in checking allowance for qty and amount 2024-03-13 13:00:10 +05:30
Nabin Hait
d279e23623 fix: added index for price_list column in Item Price 2024-03-13 12:58:13 +05:30
Nabin Hait
6ff9e6ee84 perf: Get bin details only for stock items 2024-03-13 12:57:33 +05:30
Nabin Hait
c15b2d5490 perf: validate expense against budget only if budget exists 2024-03-13 12:56:28 +05:30
Nabin Hait
49c74369a5 perf: refactored handling provisional gl entries for non-stock items 2024-03-13 12:54:13 +05:30
ruthra kumar
92ccef0ebc chore: resolve conflicts 2024-03-13 12:14:33 +05:30
ruthra kumar
ae6d1c14cb chore: better popup message
(cherry picked from commit 445d2acf50)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2024-03-13 06:12:45 +00:00
ruthra kumar
b746493830 test: cr note flag to update self
(cherry picked from commit ce3b1f09f5)
2024-03-13 06:12:44 +00:00
ruthra kumar
34053ec590 refactor: make AR/AP report aware of always standalone cr/dr notes
(cherry picked from commit 4d0c2d8e82)
2024-03-13 06:12:44 +00:00
ruthra kumar
adbc20736e chore: patch for updating flag in Cr/Dr notes
(cherry picked from commit 849f478894)
2024-03-13 06:12:44 +00:00
ruthra kumar
d1e3b2ab6e refactor: post ledger entries based on toggle
(cherry picked from commit 77aac6f571)
2024-03-13 06:12:44 +00:00
ruthra kumar
6d5e595440 refactor: checkbox in purchase invoice
(cherry picked from commit 767f2157e6)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
2024-03-13 06:12:43 +00:00
ruthra kumar
1ddb162ef1 chore: update popup message
(cherry picked from commit adf13a19c4)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2024-03-13 06:12:43 +00:00
ruthra kumar
4d6b56e55b refactor: checkbox to toggle always standalone credit note
(cherry picked from commit 2cefe2a20e)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
2024-03-13 06:12:43 +00:00
Nabin Hait
34c3e75d5c fix: convert str to date 2024-03-12 12:16:41 +05:30
Nabin Hait
ac0dd5083f fix: linter issues 2024-03-12 12:10:55 +05:30
Nabin Hait
668a93128b fix: linter issues 2024-03-12 12:02:23 +05:30
Nabin Hait
5fcf8c588a fix: Book depreciation until the asset disposal date and removed unwanted commits 2024-03-12 11:56:47 +05:30
Nabin Hait
05385e4acb perf: skip unnecessary validation while transaction cancellation 2024-03-06 22:33:36 +05:30
Nabin Hait
5cd9bf3bda fix: minor fixes 2024-03-06 21:04:07 +05:30
Nabin Hait
e4bd173875 perf: Cache accounting dimension filter map 2024-03-06 20:32:47 +05:30
Nabin Hait
acc0b2faf8 fix: linter issues 2024-03-06 17:43:27 +05:30
Nabin Hait
aa75a60142 perf: Optimzed code for merging similar gl entries 2024-03-04 16:04:39 +05:30
Nabin Hait
8cd8b8f885 perf: Cached accounting dimensions details 2024-03-04 16:04:39 +05:30
Nabin Hait
f204d810bb perf: Performance optimization for validating budget 2024-03-04 16:04:39 +05:30
Nabin Hait
d7b738ff61 perf: Optimization for providional gl entries 2024-03-04 16:04:35 +05:30
Nabin Hait
c7ed854850 fix: Reload document on cancel to avoid document modified error 2024-02-26 14:49:32 +05:30
Nabin Hait
23e256aedf fix: Patch to remove cancelled asset capitalization from asset 2024-02-26 12:53:06 +05:30
47 changed files with 847 additions and 339 deletions

View File

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

View File

@@ -236,14 +236,16 @@ def get_accounting_dimensions(as_list=True, filters=None):
def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent""",
as_dict=1,
)
if frappe.flags.accounting_dimensions_details is None:
# nosemgrep
frappe.flags.accounting_dimensions_details = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent""",
as_dict=1,
)
return dimensions
return frappe.flags.accounting_dimensions_details
def get_dimension_with_children(doctype, dimensions):

View File

@@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self):
disable_dimension()
frappe.flags.accounting_dimensions_details = None
frappe.flags.dimension_filter_map = None
def create_dimension():

View File

@@ -38,37 +38,41 @@ class AccountingDimensionFilter(Document):
def get_dimension_filter_map():
filters = frappe.db.sql(
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabAccounting Dimension Filter` p
WHERE
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
""",
as_dict=1,
)
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
if not frappe.flags.get("dimension_filter_map"):
# nosemgrep
filters = frappe.db.sql(
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabAccounting Dimension Filter` p
WHERE
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
""",
as_dict=1,
)
return dimension_filter_map
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
frappe.flags.dimension_filter_map = dimension_filter_map
return frappe.flags.dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):

View File

@@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def tearDown(self):
disable_dimension_filter()
disable_dimension()
frappe.flags.accounting_dimensions_details = None
frappe.flags.dimension_filter_map = None
for si in self.invoice_list:
si.load_from_db()

View File

@@ -109,6 +109,8 @@ class Budget(Document):
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args)
if not frappe.get_all("Budget", limit=1):
return
if args.get("company") and not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
@@ -116,6 +118,11 @@ def validate_expense_against_budget(args, expense_amount=0):
"Company", args.get("company"), "exception_budget_approver_role"
)
if not frappe.get_cached_value(
"Budget", {"fiscal_year": args.fiscal_year, "company": args.company}
): # nosec
return
if not args.account:
args.account = args.get("expense_account")
@@ -142,13 +149,13 @@ def validate_expense_against_budget(args, expense_amount=0):
if (
args.get(budget_against)
and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
):
doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
condition = """and exists(select name from `tab%s`
where lft<=%s and rgt>=%s and name=b.%s)""" % (
doctype,

View File

@@ -1,94 +1,42 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-05-16 11:54:09.286135",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2016-05-16 11:54:09.286135",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"budget_amount"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1,
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "budget_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Budget Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "budget_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Budget Amount",
"options": "Company:company:default_currency",
"reqd": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-01-02 17:02:53.339420",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2024-03-04 15:43:27.016947",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -3,22 +3,36 @@
frappe.ui.form.on("Currency Exchange Settings", {
service_provider: function (frm) {
if (frm.doc.service_provider == "exchangerate.host") {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, "https://api.exchangerate.host/convert", params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
}
frm.call({
method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
args: {
service_provider: frm.doc.service_provider,
use_http: frm.doc.use_http,
},
callback: function (r) {
if (r && r.message) {
if (frm.doc.service_provider == "exchangerate.host") {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
add_param(frm, r.message, params, result);
}
}
},
});
},
use_http: function (frm) {
frm.trigger("service_provider");
},
});

View File

@@ -9,6 +9,7 @@
"disabled",
"service_provider",
"api_endpoint",
"use_http",
"access_key",
"url",
"column_break_3",
@@ -91,12 +92,19 @@
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
},
{
"default": "0",
"depends_on": "eval: doc.service_provider != \"Custom\"",
"fieldname": "use_http",
"fieldtype": "Check",
"label": "Use HTTP Protocol"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-10-04 15:30:25.333860",
"modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"})
@@ -40,7 +40,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rates"})
self.append("result_key", {"key": "{to_currency}"})
self.append("req_params", {"key": "base", "value": "{from_currency}"})
@@ -79,3 +79,19 @@ class CurrencyExchangeSettings(Document):
frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url
@frappe.whitelist()
def get_api_endpoint(service_provider: str = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:
protocol = "http://"
return protocol + api
return None

View File

@@ -606,21 +606,21 @@ def get_account_details(
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
)
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)
):
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)
return account_details

View File

@@ -174,7 +174,8 @@
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
"label": "Voucher Detail No",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "project",
@@ -256,7 +257,8 @@
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"modified": "2020-04-07 16:22:33.766994",
"links": [],
"modified": "2024-03-19 18:30:49.613401",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
@@ -288,5 +290,6 @@
"quick_entry": 1,
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified",
"sort_order": "DESC"
}
"sort_order": "DESC",
"states": []
}

View File

@@ -127,7 +127,7 @@ class GLEntry(Document):
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
account_type = frappe.get_cached_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (
@@ -159,7 +159,7 @@ class GLEntry(Document):
def check_pl_account(self):
if (
self.is_opening == "Yes"
and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss"
and not self.is_cancelled
):
frappe.throw(
@@ -252,7 +252,7 @@ class GLEntry(Document):
def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be")
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
@@ -279,7 +279,7 @@ def update_outstanding_amt(
party_condition = ""
if against_voucher_type == "Sales Invoice":
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to")
account_condition = "and account in ({0}, {1})".format(
frappe.db.escape(account), frappe.db.escape(party_account)
)
@@ -347,7 +347,7 @@ def update_outstanding_amt(
def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value(
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", None, "frozen_accounts_modifier"
)

View File

@@ -350,7 +350,9 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
if ref_exchange_rate:
# Only update exchange rate when the reference is Journal Entry
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():

View File

@@ -132,11 +132,12 @@ class PaymentLedgerEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
if not self.delinked:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
# update outstanding amount
if (

View File

@@ -1130,6 +1130,17 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
pr.reconcile()
si.reload()
self.assertEqual(si.outstanding_amount, 0)
# No Exchange Gain/Loss journal should be generated
exc_gain_loss_journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1},
fields=["parent"],
)
self.assertEqual(exc_gain_loss_journals, [])
def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)

View File

@@ -89,10 +89,11 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
<th style="width: 20%">0 - 30 Days</th>
<th style="width: 20%">30 - 60 Days</th>
<th style="width: 20%">60 - 90 Days</th>
<th style="width: 20%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -101,6 +102,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@@ -22,6 +22,7 @@
"is_paid",
"is_return",
"return_against",
"update_outstanding_for_self",
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
@@ -1604,13 +1605,21 @@
"fieldtype": "Check",
"label": "Use Transaction Date Exchange Rate",
"read_only": 1
},
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-02-25 11:20:28.366808",
"modified": "2024-03-11 14:46:30.298184",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -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
@@ -575,13 +575,12 @@ class PurchaseInvoice(BuyingController):
self.db_set("repost_required", self.needs_repost)
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
if gl_entries:
make_gl_entries(
gl_entries,
update_outstanding=update_outstanding,
@@ -589,29 +588,43 @@ class PurchaseInvoice(BuyingController):
from_repost=from_repost,
)
self.make_exchange_gain_loss_journal()
elif self.docstatus == 2:
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if provisional_entries:
for entry in provisional_entries:
frappe.db.set_value(
"GL Entry",
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
"is_cancelled",
1,
)
if update_outstanding == "No":
update_outstanding_amt(
self.credit_to,
"Supplier",
self.supplier,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
)
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
self.cancel_provisional_entries()
self.update_supplier_outstanding(update_outstanding)
def cancel_provisional_entries(self):
rows = set()
purchase_receipts = set()
for d in self.items:
if d.purchase_receipt:
purchase_receipts.add(d.purchase_receipt)
rows.add(d.name)
if rows:
# cancel gl entries
gle = qb.DocType("GL Entry")
gle_update_query = (
qb.update(gle)
.set(gle.is_cancelled, 1)
.where(
(gle.voucher_type == "Purchase Receipt")
& (gle.voucher_no.isin(purchase_receipts))
& (gle.voucher_detail_no.isin(rows))
)
)
gle_update_query.run()
def update_supplier_outstanding(self, update_outstanding):
if update_outstanding == "No":
update_outstanding_amt(
self.credit_to,
"Supplier",
self.supplier,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
)
def get_gl_entries(self, warehouse_account=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
@@ -663,6 +676,10 @@ class PurchaseInvoice(BuyingController):
)
if grand_total and not self.is_internal_transfer():
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
@@ -676,7 +693,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"against_voucher": self.name,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
@@ -720,8 +737,8 @@ class PurchaseInvoice(BuyingController):
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
)
)
purchase_receipt_doc_map = {}
if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts()
for item in self.get("items"):
if flt(item.base_net_amount):
@@ -858,44 +875,7 @@ class PurchaseInvoice(BuyingController):
dummy, amount = self.get_amount_and_base_amount(item, None)
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
"Purchase Receipt Item",
item.pr_detail,
["provisional_expense_account", "qty", "base_rate"],
)
provisional_account = provisional_account or self.get_company_default(
"default_provisional_account"
)
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value(
"GL Entry",
{
"is_cancelled": 0,
"voucher_type": "Purchase Receipt",
"voucher_no": item.purchase_receipt,
"voucher_detail_no": item.pr_detail,
"account": provisional_account,
},
"name",
)
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
item,
gl_entries,
self.posting_date,
provisional_account,
reverse=1,
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
)
self.make_provisional_gl_entry(gl_entries, item)
if not self.is_internal_transfer():
gl_entries.append(
@@ -992,6 +972,57 @@ class PurchaseInvoice(BuyingController):
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_gross_purchase_amount_for_linked_assets(item)
def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict()
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
pr_items = frappe.get_all(
"Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)},
fields=["name", "provisional_expense_account", "qty", "base_rate"],
)
default_provisional_account = self.get_company_default("default_provisional_account")
provisional_accounts = set(
[
d.provisional_expense_account if d.provisional_expense_account else default_provisional_account
for d in pr_items
]
)
provisional_gl_entries = frappe.get_all(
"GL Entry",
filters={
"voucher_type": "Purchase Receipt",
"voucher_no": ("in", linked_purchase_receipts),
"account": ("in", provisional_accounts),
"is_cancelled": 0,
},
fields=["voucher_detail_no"],
)
rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
for item in pr_items:
self.provisional_accounts[item.name] = {
"provisional_account": item.provisional_expense_account or default_provisional_account,
"qty": item.qty,
"base_rate": item.base_rate,
"has_provisional_entry": item.name in rows_with_provisional_entries,
}
def make_provisional_gl_entry(self, gl_entries, item):
if item.purchase_receipt:
pr_item = self.provisional_accounts.get(item.pr_detail, {})
if pr_item.get("has_provisional_entry"):
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
item,
gl_entries,
self.posting_date,
pr_item.get("provisional_account"),
reverse=1,
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
)
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all(
"Asset",

View File

@@ -740,6 +740,7 @@
"fieldtype": "Currency",
"label": "Landed Cost Voucher Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -893,7 +894,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-12-25 22:00:28.043555",
"modified": "2024-03-19 19:09:47.210965",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -903,4 +904,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -25,6 +25,7 @@
"is_consolidated",
"is_return",
"return_against",
"update_outstanding_for_self",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"is_debit_note",
@@ -2161,6 +2162,15 @@
"fieldtype": "Check",
"label": "Don't Create Loyalty Points",
"no_copy": 1
},
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self",
"no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -2173,7 +2183,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-01-02 17:25:46.027523",
"modified": "2024-03-15 16:44:17.778370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1059,6 +1059,10 @@ class SalesInvoice(SellingController):
)
if grand_total and not self.is_internal_transfer():
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
@@ -1072,7 +1076,7 @@ class SalesInvoice(SellingController):
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"against_voucher": self.name,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,

View File

@@ -1569,6 +1569,12 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_zero_qty_return_invoice_with_stock_effect(self):
cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
cr_note.update_stock = True
cr_note.items[0].qty = 0
self.assertRaises(frappe.ValidationError, cr_note.save)
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",

View File

@@ -7,7 +7,7 @@ import copy
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, getdate, now
from frappe.utils import cint, flt, formatdate, getdate, now
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -234,11 +234,13 @@ def get_cost_center_allocation_data(company, posting_date):
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
merge_properties = get_merge_properties(accounting_dimensions)
for entry in gl_map:
entry.merge_key = get_merge_key(entry, merge_properties)
# if there is already an entry in this account then just add it
# to that entry
same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions)
same_head = check_if_in_list(entry, merged_gl_map)
if same_head:
same_head.debit = flt(same_head.debit) + flt(entry.debit)
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
@@ -273,33 +275,34 @@ def merge_similar_entries(gl_map, precision=None):
return merged_gl_map
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = [
"voucher_detail_no",
"party",
"against_voucher",
def get_merge_properties(dimensions=None):
merge_properties = [
"account",
"cost_center",
"against_voucher_type",
"party",
"party_type",
"voucher_detail_no",
"against_voucher",
"against_voucher_type",
"project",
"finance_book",
]
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
merge_properties.extend(dimensions)
return merge_properties
def get_merge_key(entry, merge_properties):
merge_key = []
for fieldname in merge_properties:
merge_key.append(entry.get(fieldname, ""))
return tuple(merge_key)
def check_if_in_list(gle, gl_map):
for e in gl_map:
same_head = True
if e.account != gle.account:
same_head = False
continue
for fieldname in account_head_fieldnames:
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
same_head = False
break
if same_head:
if e.merge_key == gle.merge_key:
return e

View File

@@ -692,7 +692,12 @@ class ReceivablePayableReport(object):
def get_return_entries(self):
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
filters = {
"is_return": 1,
"docstatus": 1,
"company": self.filters.company,
"update_outstanding_for_self": 0,
}
or_filters = {}
for party_type in self.party_type:
party_field = scrub(party_type)

View File

@@ -62,7 +62,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
pe.insert()
pe.submit()
def create_credit_note(self, docname):
def create_credit_note(self, docname, do_not_submit=False):
credit_note = create_sales_invoice(
company=self.company,
customer=self.customer,
@@ -72,6 +72,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
cost_center=self.cost_center,
is_return=1,
return_against=docname,
do_not_submit=do_not_submit,
)
return credit_note
@@ -149,7 +150,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
self.create_credit_note(si.name)
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
@@ -167,6 +170,82 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
],
)
def test_cr_note_flag_to_update_self(self):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.set_posting_time = True
si.posting_date = add_days(today(), -1)
si.save().submit()
report = execute(filters)
expected_data = [100, 100, "No Remarks"]
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [100, 100, 40, 60]
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual(
expected_data_after_payment,
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = True
cr_note.save().submit()
report = execute(filters)
expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = [
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.voucher_no,
]
for row in report[1]
if row.voucher_no == si.name
][0]
cr_note_row = [
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.voucher_no,
]
for row in report[1]
if row.voucher_no == cr_note.name
][0]
self.assertEqual(expected_data_after_credit_note[0], si_row)
self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
def test_payment_againt_po_in_receivable_report(self):
"""
Payments made against Purchase Order will show up as outstanding amount

View File

@@ -57,6 +57,7 @@ class Asset(AccountsController):
self.validate_in_use_date()
self.set_status()
self.make_asset_movement()
self.reload()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
@@ -64,6 +65,7 @@ class Asset(AccountsController):
self.validate_cancellation()
self.cancel_movement_entries()
self.cancel_capitalization()
self.reload()
self.delete_depreciation_entries()
self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
@@ -428,7 +430,7 @@ class Asset(AccountsController):
schedule_date = get_last_day(schedule_date)
# if asset is being sold
if date_of_disposal:
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
from_date = self.get_from_date_for_disposal(finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book,
@@ -1009,7 +1011,9 @@ class Asset(AccountsController):
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()
purchase_document
and self.purchase_receipt_amount
and getdate(self.available_for_use_date) <= getdate()
):
gl_entries.append(

View File

@@ -210,9 +210,7 @@ def make_depreciation_entry(
debit_account,
accounting_dimensions,
)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
depreciation_posting_error = e
asset.set_status()
@@ -474,6 +472,7 @@ def depreciate_asset(asset, date):
make_depreciation_entry(asset.name, date)
asset.reload()
cancel_depreciation_entries(asset, date)

View File

@@ -84,6 +84,10 @@ class AssetCapitalization(StockController):
self.make_gl_entries()
self.restore_consumed_asset_items()
def on_trash(self):
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
super(AssetCapitalization, self).on_trash()
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
asset_doc = frappe.get_doc("Asset", self.target_asset)

View File

@@ -77,7 +77,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldname: "group_by",
label: __("Group by"),
fieldtype: "Select",
options: [__("Group by Supplier"), __("Group by Item")],
options: [
{ label: __("Group by Supplier"), value: "Group by Supplier" },
{ label: __("Group by Item"), value: "Group by Item" },
],
default: __("Group by Supplier"),
},
{

View File

@@ -88,6 +88,7 @@ force_item_fields = (
"weight_per_unit",
"weight_uom",
"total_weight",
"valuation_rate",
)
@@ -156,6 +157,13 @@ class AccountsController(TransactionBase):
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"]
and self.get("is_return")
and self.get("update_stock")
):
self.validate_zero_qty_for_return_invoices_with_stock()
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
@@ -203,17 +211,18 @@ class AccountsController(TransactionBase):
)
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
# if self.get("is_return") and self.get("return_against"):
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
frappe.msgprint(
_(
"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
).format(
document_type,
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
if self.get("update_outstanding_for_self"):
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
frappe.msgprint(
_(
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
frappe.bold("Update Outstanding for Self"),
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
)
)
)
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
@@ -948,6 +957,18 @@ class AccountsController(TransactionBase):
return gl_dict
def validate_zero_qty_for_return_invoices_with_stock(self):
rows = []
for item in self.items:
if not flt(item.qty):
rows.append(item)
if rows:
frappe.throw(
_(
"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
)
def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
return
@@ -1605,8 +1626,8 @@ class AccountsController(TransactionBase):
item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None
role_allowed_to_over_bill = frappe.db.get_single_value(
"Accounts Settings", "role_allowed_to_over_bill"
role_allowed_to_over_bill = frappe.get_cached_value(
"Accounts Settings", None, "role_allowed_to_over_bill"
)
user_roles = frappe.get_roles()

View File

@@ -573,6 +573,7 @@ class StatusUpdater(Document):
ref_doc.set_status(update=True)
@frappe.request_cache
def get_allowance_for(
item_code,
item_allowance=None,
@@ -602,20 +603,20 @@ def get_allowance_for(
global_amount_allowance,
)
qty_allowance, over_billing_allowance = frappe.db.get_value(
qty_allowance, over_billing_allowance = frappe.get_cached_value(
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
)
if qty_or_amount == "qty" and not qty_allowance:
if global_qty_allowance == None:
global_qty_allowance = flt(
frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
)
qty_allowance = global_qty_allowance
elif qty_or_amount == "amount" and not over_billing_allowance:
if global_amount_allowance == None:
global_amount_allowance = flt(
frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance")
)
over_billing_allowance = global_amount_allowance

View File

@@ -5,6 +5,7 @@ import unittest
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.utils import FrappeTestCase
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings,
@@ -19,7 +20,7 @@ from erpnext.e_commerce.shopping_cart.cart import get_party
from erpnext.stock.doctype.item.test_item import make_item
class TestItemReview(unittest.TestCase):
class TestItemReview(FrappeTestCase):
def setUp(self):
item = make_item("Test Mobile Phone")
if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
@@ -29,8 +30,7 @@ class TestItemReview(unittest.TestCase):
frappe.local.shopping_cart_settings = None
def tearDown(self):
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
setup_e_commerce_settings({"enable_reviews": 0})
frappe.db.rollback()
def test_add_and_get_item_reviews_from_customer(self):
"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
@@ -44,7 +44,7 @@ class TestItemReview(unittest.TestCase):
# post review on "Test Mobile Phone"
try:
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
add_item_review(web_item, "Great Product", 1, "Would recommend this product")
review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
except Exception:
self.fail(f"Error while publishing review for {web_item}")
@@ -52,8 +52,7 @@ class TestItemReview(unittest.TestCase):
review_data = get_item_reviews(web_item, 0, 10)
self.assertEqual(len(review_data.reviews), 1)
self.assertEqual(review_data.average_rating, 3)
self.assertEqual(review_data.reviews_per_rating[2], 100)
self.assertEqual(review_data.average_rating, 1)
# tear down
frappe.set_user("Administrator")

View File

@@ -24,10 +24,11 @@ WEBITEM_PRICE_TESTS = (
"test_website_item_price_for_guest_user",
)
from frappe.tests.utils import FrappeTestCase
class TestWebsiteItem(unittest.TestCase):
@classmethod
def setUpClass(cls):
class TestWebsiteItem(FrappeTestCase):
def setUp(self):
setup_e_commerce_settings(
{
"company": "_Test Company",
@@ -37,11 +38,6 @@ class TestWebsiteItem(unittest.TestCase):
}
)
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def setUp(self):
if self._testMethodName in WEBITEM_DESK_TESTS:
make_item(
"Test Web Item",
@@ -75,6 +71,9 @@ class TestWebsiteItem(unittest.TestCase):
customer="_Test Customer",
)
def tearDown(self):
frappe.db.rollback()
def test_index_creation(self):
"Check if index is getting created in db."
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update

View File

@@ -354,9 +354,11 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v14_0.update_flag_for_return_invoices
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
cancelled_asset_capitalizations = frappe.get_all(
"Asset Capitalization",
filters={"docstatus": 2},
fields=["name", "target_asset"],
)
for asset_capitalization in cancelled_asset_capitalizations:
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)

View File

@@ -0,0 +1,62 @@
from frappe import qb
def execute():
# Set "update_outstanding_for_self" flag in Credit/Debit Notes
# Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves.
gle = qb.DocType("GL Entry")
# Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport
# https://github.com/frappe/erpnext/pull/39497
creation_date = "2024-01-25"
si = qb.DocType("Sales Invoice")
if cr_notes := (
qb.from_(si)
.select(si.name)
.where(
(si.creation.gte(creation_date))
& (si.docstatus == 1)
& (si.is_return == True)
& (si.return_against.notnull())
)
.run()
):
cr_notes = [x[0] for x in cr_notes]
if docs_that_require_update := (
qb.from_(gle)
.select(gle.voucher_no)
.distinct()
.where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher))
.run()
):
docs_that_require_update = [x[0] for x in docs_that_require_update]
qb.update(si).set(si.update_outstanding_for_self, True).where(
si.name.isin(docs_that_require_update)
).run()
pi = qb.DocType("Purchase Invoice")
if dr_notes := (
qb.from_(pi)
.select(pi.name)
.where(
(pi.creation.gte(creation_date))
& (pi.docstatus == 1)
& (pi.is_return == True)
& (pi.return_against.notnull())
)
.run()
):
dr_notes = [x[0] for x in dr_notes]
if docs_that_require_update := (
qb.from_(gle)
.select(gle.voucher_no)
.distinct()
.where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher))
.run()
):
docs_that_require_update = [x[0] for x in docs_that_require_update]
qb.update(pi).set(pi.update_outstanding_for_self, True).where(
pi.name.isin(docs_that_require_update)
).run()

View File

@@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(sales_order.get("payment_schedule"))
def test_gross_profit(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import insert_item_price
item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1})
item_code = item_doc.name
make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC")
selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
insert_item_price(
frappe._dict(
{
"item_code": item_code,
"price_list": selling_price_list,
"price_list_rate": 300,
"rate": 300,
"conversion_factor": 1,
"discount_amount": 0.0,
"currency": frappe.db.get_value("Price List", selling_price_list, "currency"),
"uom": item_doc.stock_uom,
}
)
)
quotation = make_quotation(
item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list
)
self.assertEqual(quotation.items[0].valuation_rate, 100)
self.assertEqual(quotation.items[0].gross_profit, 200)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def test_maintain_rate_in_sales_cycle_is_enforced(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order

View File

@@ -1305,7 +1305,11 @@ def create_pick_list(source_name, target_doc=None):
"Sales Order",
source_name,
{
"Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
"Sales Order": {
"doctype": "Pick List",
"field_map": {"set_warehouse": "parent_warehouse"},
"validation": {"docstatus": ["=", 1]},
},
"Sales Order Item": {
"doctype": "Pick List Item",
"field_map": {"parent": "sales_order", "name": "sales_order_item"},

View File

@@ -2160,6 +2160,40 @@ class TestSalesOrder(FrappeTestCase):
self.assertFalse(row.warehouse == rejected_warehouse)
self.assertTrue(row.warehouse == warehouse)
def test_auto_update_price_list(self):
item = make_item(
"_Test Auto Update Price List Item",
)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 100)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 100)
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 200)
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
@@ -2225,13 +2259,14 @@ def make_sales_order(**args):
return so
def create_dn_against_so(so, delivered_qty=0):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
dn = make_delivery_note(so)
dn.get("items")[0].qty = delivered_qty or 5
dn.insert()
dn.submit()
if not do_not_submit:
dn.submit()
return dn

View File

@@ -0,0 +1,92 @@
[
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU001",
"item_name": "T-shirt",
"valuation_rate": 400.0,
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU002",
"valuation_rate": 300.0,
"item_name": "Laptop",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU003",
"valuation_rate": 523.0,
"item_name": "Book",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU004",
"valuation_rate": 725.0,
"item_name": "Smartphone",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU005",
"valuation_rate": 222.0,
"item_name": "Sneakers",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU006",
"valuation_rate": 420.0,
"item_name": "Coffee Mug",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU007",
"valuation_rate": 375.0,
"item_name": "Television",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU008",
"valuation_rate": 333.0,
"item_name": "Backpack",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU009",
"valuation_rate": 700.0,
"item_name": "Headphones",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg"
},
{
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU010",
"valuation_rate": 500.0,
"item_name": "Camera",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg"
}
]

View File

@@ -130,6 +130,7 @@ class DeliveryNote(SellingController):
def validate(self):
self.validate_posting_time()
super(DeliveryNote, self).validate()
self.validate_references()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -195,6 +196,58 @@ class DeliveryNote(SellingController):
]
)
def validate_references(self):
self.validate_sales_order_references()
self.validate_sales_invoice_references()
def validate_sales_order_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_order and not item.so_detail) or (
not item.against_sales_order and item.so_detail
):
if not item.against_sales_order:
err_msg += (
_("'Sales Order' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_order")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("so_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
def validate_sales_invoice_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_invoice and not item.si_detail) or (
not item.against_sales_invoice and item.si_detail
):
if not item.against_sales_invoice:
err_msg += (
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("si_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:

View File

@@ -723,6 +723,15 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
def test_sales_order_reference_validation(self):
so = make_sales_order(po_no="12345")
dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
dn.items[0].against_sales_order = None
self.assertRaises(frappe.ValidationError, dn.save)
dn.reload()
dn.items[0].so_detail = None
self.assertRaises(frappe.ValidationError, dn.save)
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order(po_no="12345")

View File

@@ -104,7 +104,8 @@
"in_standard_filter": 1,
"label": "Price List",
"options": "Price List",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
@@ -220,7 +221,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-15 08:26:04.041861",
"modified": "2024-03-13 12:23:39.630290",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Price",

View File

@@ -188,9 +188,9 @@ class PickList(Document):
picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict()
from_warehouses = None
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
if self.parent_warehouse:
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse))
# Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get("locations")

View File

@@ -226,7 +226,7 @@
},
{
"fieldname": "stock_queue",
"fieldtype": "Text",
"fieldtype": "Long Text",
"label": "FIFO Stock Queue (qty, rate)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
@@ -324,7 +324,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-23 18:07:42.063615",
"modified": "2024-03-13 09:56:13.021696",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -104,22 +104,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
if (
args.get("doctype") == "Material Request"
and args.get("material_request_type") == "Material Transfer"
):
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
if doc and doc.get("doctype") == "Purchase Order":
# calculate company_total_stock only for po
bin_details = get_bin_details(
args.item_code, out.warehouse, args.company, include_child_warehouses=True
)
else:
bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
out.update(bin_details)
if item.is_stock_item:
update_bin_details(args, out, doc)
# update args with out, if key or value not exists
for key, value in out.items():
@@ -202,6 +188,24 @@ def set_valuation_rate(out, args):
out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse")))
def update_bin_details(args, out, doc):
if (
args.get("doctype") == "Material Request"
and args.get("material_request_type") == "Material Transfer"
):
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None
# calculate company_total_stock only for po
bin_details = get_bin_details(
args.item_code, out.warehouse, company, include_child_warehouses=True
)
out.update(bin_details)
def process_args(args):
if isinstance(args, str):
args = json.loads(args)
@@ -850,7 +854,9 @@ 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:
if price_list_rate is None or frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate"
):
if args.price_list and args.rate:
insert_item_price(args)
return out