Compare commits

...

60 Commits

Author SHA1 Message Date
Frappe PR Bot
9397a57d4d chore(release): Bumped to Version 15.55.5
## [15.55.5](https://github.com/frappe/erpnext/compare/v15.55.4...v15.55.5) (2025-04-07)

### Bug Fixes

* set draft QC in purchase document on creation of qc ([bf3349a](bf3349a432))
* slow query ([b172ae0](b172ae0557))
2025-04-07 05:14:16 +00:00
rohitwaghchaure
d70050931b Merge pull request #46887 from frappe/mergify/bp/version-15/pr-46842
fix: set draft QC in purchase document on creation of qc (backport #46832) (backport #46842)
2025-04-07 10:42:55 +05:30
rohitwaghchaure
1875d69f60 Merge pull request #46888 from frappe/mergify/bp/version-15/pr-46880
fix: slow query (backport #46845) (backport #46880)
2025-04-07 10:42:41 +05:30
rohitwaghchaure
b2d71b44cf chore: fix conflicts
(cherry picked from commit 4bcf052220)
2025-04-07 04:56:07 +00:00
Rohit Waghchaure
b172ae0557 fix: slow query
(cherry picked from commit f82c8ea5eb)

# Conflicts:
#	erpnext/stock/deprecated_serial_batch.py
(cherry picked from commit 23dc9d5872)
2025-04-07 04:56:07 +00:00
Rohit Waghchaure
bf3349a432 fix: set draft QC in purchase document on creation of qc
(cherry picked from commit 2553dea78e)
(cherry picked from commit 54159b9e5e)
2025-04-07 04:55:58 +00:00
Frappe PR Bot
5dd99f896e chore(release): Bumped to Version 15.55.4
## [15.55.4](https://github.com/frappe/erpnext/compare/v15.55.3...v15.55.4) (2025-03-29)

### Bug Fixes

* valuation rate not updating for raw materials ([57e2619](57e2619cf1))
2025-03-29 20:07:32 +00:00
rohitwaghchaure
7579e00425 Merge pull request #46790 from frappe/mergify/bp/version-15/pr-46778
fix: valuation rate not updating for raw materials (backport #46760) (backport #46778)
2025-03-30 01:36:12 +05:30
rohitwaghchaure
c22869fed9 chore: fix conflicts
(cherry picked from commit 5079519863)
2025-03-29 15:31:21 +00:00
Rohit Waghchaure
57e2619cf1 fix: valuation rate not updating for raw materials
(cherry picked from commit 5af8378471)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
(cherry picked from commit 454dd3a2f1)
2025-03-29 15:31:21 +00:00
Frappe PR Bot
66d0ad1bc6 chore(release): Bumped to Version 15.55.3
## [15.55.3](https://github.com/frappe/erpnext/compare/v15.55.2...v15.55.3) (2025-03-29)

### Bug Fixes

* incorrect condition ([0721816](0721816763))
2025-03-29 07:03:05 +00:00
rohitwaghchaure
3395e7c2cd Merge pull request #46785 from frappe/mergify/bp/version-15/pr-46781
fix: incorrect condition (backport #46777) (backport #46781)
2025-03-29 12:31:36 +05:30
Rohit Waghchaure
0721816763 fix: incorrect condition
(cherry picked from commit 0c1a8e9c58)
(cherry picked from commit 502b8f25b3)
2025-03-29 06:48:03 +00:00
rohitwaghchaure
e45d0779ef Merge pull request #46752 from frappe/mergify/bp/version-15/pr-46749
Revert "perf: timeout while renaming cost center (backport #46641)" (backport #46749)
2025-03-27 13:13:17 +05:30
rohitwaghchaure
c6ce76170b Revert "perf: timeout while renaming cost center (backport #46641)"
(cherry picked from commit 326126e741)
2025-03-27 06:54:18 +00:00
Frappe PR Bot
de3e6922b5 chore(release): Bumped to Version 15.55.2
## [15.55.2](https://github.com/frappe/erpnext/compare/v15.55.1...v15.55.2) (2025-03-27)

### Bug Fixes

* do not use self object for setting party and party type ([7795030](7795030b7b))
2025-03-27 06:05:55 +00:00
ruthra kumar
ae6d3f27a2 Merge pull request #46747 from frappe/mergify/bp/version-15/pr-46719
fix: do not use self object for setting party and party type  (backport #46719)
2025-03-27 11:34:32 +05:30
ljain112
7795030b7b fix: do not use self object for setting party and party type
(cherry picked from commit 80b746d4dd)
2025-03-27 05:39:52 +00:00
Frappe PR Bot
9bac43acff chore(release): Bumped to Version 15.55.1
## [15.55.1](https://github.com/frappe/erpnext/compare/v15.55.0...v15.55.1) (2025-03-27)

### Bug Fixes

* decimal values causing incorrect batch picking ([1b6aeba](1b6aeba267))
* expense account in the stock entry ([e393ce9](e393ce9a47))
* slow query ([f3ba5a8](f3ba5a81ab))
2025-03-27 03:45:26 +00:00
rohitwaghchaure
1e987153c9 Merge pull request #46735 from frappe/mergify/bp/version-15/pr-46730
fix: expense account in the stock entry (backport #46710) (backport #46730)
2025-03-27 09:14:05 +05:30
rohitwaghchaure
d36a7c2389 Merge pull request #46741 from frappe/mergify/bp/version-15/pr-46734
fix: decimal values causing incorrect batch picking (backport #46733) (backport #46734)
2025-03-27 09:13:56 +05:30
rohitwaghchaure
b548cc411d Merge pull request #46742 from frappe/mergify/bp/version-15/pr-46740
fix: slow query (backport #46739) (backport #46740)
2025-03-27 09:13:47 +05:30
rohitwaghchaure
ad3f985dc4 chore: fix conflicts
(cherry picked from commit 41f20a9c64)
2025-03-26 17:09:42 +00:00
Rohit Waghchaure
f3ba5a81ab fix: slow query
(cherry picked from commit 5ddb36af87)

# Conflicts:
#	erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
(cherry picked from commit af0fb131a2)
2025-03-26 17:09:42 +00:00
Rohit Waghchaure
1b6aeba267 fix: decimal values causing incorrect batch picking
(cherry picked from commit 7bfe703b04)
(cherry picked from commit c5efddae16)
2025-03-26 16:43:35 +00:00
Rohit Waghchaure
e393ce9a47 fix: expense account in the stock entry
(cherry picked from commit 89569d4b32)
(cherry picked from commit 62f342ef8b)
2025-03-26 10:18:49 +00:00
Frappe PR Bot
8951efb457 chore(release): Bumped to Version 15.55.0
# [15.55.0](https://github.com/frappe/erpnext/compare/v15.54.5...v15.55.0) (2025-03-25)

### Bug Fixes

* add base_outstanding and base_paid_amount in payment schedule table ([412e6be](412e6be502))
* add patch to update base_outstanding and base_paid_amount ([c3221c4](c3221c4e93))
* correct accumulated depreciation calculation for disposed assets (backport [#46660](https://github.com/frappe/erpnext/issues/46660)) ([#46661](https://github.com/frappe/erpnext/issues/46661)) ([4df5f18](4df5f18d85))
* correct invoice order in payment reconcillaiton ([2a70791](2a70791bba))
* customer credit limit check based on `bypass_credit_limit_check` in Journal Entry ([6c443bd](6c443bd85a))
* date added to wrong patch ([2bfaf64](2bfaf64fff))
* do not validate if conversion rate is 1 for different currency ([391b5c4](391b5c4226))
* don't filter payment entries on Bank Account in Payment Clearance ([dc3b5e2](dc3b5e2f3a))
* **Payment Entry:** get contact details from existing contact ([#40556](https://github.com/frappe/erpnext/issues/40556)) ([f964178](f964178008))
* unwired order_by argument in get_transaction_list (backport [#46636](https://github.com/frappe/erpnext/issues/46636)) ([#46643](https://github.com/frappe/erpnext/issues/46643)) ([2ebea88](2ebea8866a))

### Features

* **accounting:** allow chart_of_account.get_chart to be whilelist ([e69c722](e69c722534))
* **projects:** add option to hide timesheets for project users ([#46173](https://github.com/frappe/erpnext/issues/46173)) ([3834d6f](3834d6fbce))
* repost accounting ledger for purchase receipt ([4edfc6f](4edfc6f125))

### Performance Improvements

* timeout while renaming cost center ([58eb184](58eb1849d7))
2025-03-25 13:50:33 +00:00
ruthra kumar
5db2a19778 Merge pull request #46715 from frappe/version-15-hotfix
chore: release v15
2025-03-25 19:16:44 +05:30
ruthra kumar
288206bdcd Merge pull request #46634 from frappe/mergify/bp/version-15-hotfix/pr-46627
fix: date added to wrong patch (backport #46627)
2025-03-25 18:45:21 +05:30
ruthra kumar
b3c3733286 chore: resolve conflict 2025-03-25 17:53:29 +05:30
ruthra kumar
b2b49446d4 Merge pull request #46713 from frappe/mergify/bp/version-15-hotfix/pr-46616
fix: do not validate if conversion rate is 1 for different currency (backport #46616)
2025-03-25 14:29:09 +05:30
ljain112
391b5c4226 fix: do not validate if conversion rate is 1 for different currency
(cherry picked from commit e8a66d03bc)
2025-03-25 08:42:31 +00:00
ruthra kumar
ce454d5202 Merge pull request #46707 from frappe/mergify/bp/version-15-hotfix/pr-46617
refactor: removed redundant message display for each item row cost center update (backport #46617)
2025-03-25 12:28:43 +05:30
ljain112
f93feb18fb refactor: removed redundant message display for each item row cost center update
(cherry picked from commit 4376ca5f1d)
2025-03-25 06:47:13 +00:00
ruthra kumar
0793213981 Merge pull request #46705 from frappe/mergify/bp/version-15-hotfix/pr-46622
feat: repost accounting ledger for purchase receipt (backport #46622)
2025-03-25 12:13:56 +05:30
ljain112
4edfc6f125 feat: repost accounting ledger for purchase receipt
(cherry picked from commit b36e356469)
2025-03-25 06:19:43 +00:00
mergify[bot]
98df0614ab ci: apply label "skip-release-notes" based on PR title (backport #46694) (#46697)
ci: apply label "skip-release-notes" based on PR title (#46694)

Workflow copied from frappe/frappe

(cherry picked from commit eb350012b0)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-03-24 16:55:11 +01:00
Sagar Vora
59d0ff493f Merge pull request #46688 from frappe/mergify/bp/version-15-hotfix/pr-46669
fix: don't filter payment entries on Bank Account in Payment Clearance (backport #46669)
2025-03-24 18:17:59 +05:30
vishakhdesai
dc3b5e2f3a fix: don't filter payment entries on Bank Account in Payment Clearance
(cherry picked from commit fa2fd5bf88)
2025-03-24 12:22:09 +00:00
ruthra kumar
058a2f0c42 Merge pull request #46677 from frappe/mergify/bp/version-15-hotfix/pr-46577
fix: customer credit limit check based on `bypass_credit_limit_check` in Journal Entry (backport #46577)
2025-03-24 13:58:10 +05:30
ljain112
6c443bd85a fix: customer credit limit check based on bypass_credit_limit_check in Journal Entry
(cherry picked from commit 8a84faebed)
2025-03-24 08:02:17 +00:00
ruthra kumar
a228d1edc5 Merge pull request #46672 from frappe/mergify/bp/version-15-hotfix/pr-46574
fix: correct invoice order in payment reconcillaiton (backport #46574)
2025-03-24 13:13:35 +05:30
ruthra kumar
87628835bf Merge pull request #46673 from frappe/mergify/bp/version-15-hotfix/pr-46386
feat(accounting/regional): allow chart_of_account.get_chart to be whilelist (backport #46386)
2025-03-24 13:13:06 +05:30
Florian HENRY
e69c722534 feat(accounting): allow chart_of_account.get_chart to be whilelist
(cherry picked from commit 49dcd96909)
2025-03-24 07:03:37 +00:00
ruthra kumar
326c37a051 chore: resolve conflict 2025-03-24 12:29:52 +05:30
ljain112
2a70791bba fix: correct invoice order in payment reconcillaiton
(cherry picked from commit 5c34a5aaed)

# Conflicts:
#	erpnext/accounts/utils.py
2025-03-24 06:54:24 +00:00
ruthra kumar
e5fb77c65f Merge pull request #46670 from frappe/mergify/bp/version-15-hotfix/pr-46440
fix: add base_outstanding and base_paid_amount in payment schedule table (backport #46440)
2025-03-24 12:13:51 +05:30
ruthra kumar
4a7d401dc5 chore: resolve conflict 2025-03-24 11:53:14 +05:30
Sugesh393
c3221c4e93 fix: add patch to update base_outstanding and base_paid_amount
(cherry picked from commit 7e92e4967a)
2025-03-24 06:14:41 +00:00
Sugesh393
412e6be502 fix: add base_outstanding and base_paid_amount in payment schedule table
(cherry picked from commit 6c2f9a563e)

# Conflicts:
#	erpnext/accounts/doctype/payment_schedule/payment_schedule.json
2025-03-24 06:14:41 +00:00
mergify[bot]
4df5f18d85 fix: correct accumulated depreciation calculation for disposed assets (backport #46660) (#46661)
fix: correct accumulated depreciation calculation for disposed assets (#46660)

(cherry picked from commit eec2e7e833)

Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2025-03-23 18:53:49 +05:30
rohitwaghchaure
d58e527b6b Merge pull request #46647 from frappe/mergify/bp/version-15-hotfix/pr-46641
perf: timeout while renaming cost center (backport #46641)
2025-03-22 20:49:18 +05:30
Rohit Waghchaure
58eb1849d7 perf: timeout while renaming cost center
(cherry picked from commit 92be7cbbbf)
2025-03-21 12:38:51 +00:00
mergify[bot]
2ebea8866a fix: unwired order_by argument in get_transaction_list (backport #46636) (#46643)
* fix: unwired order_by argument

* lol on how it was updated from modified in both the places (version 15), but wasn't fixed

(cherry picked from commit 2c1077d332)

# Conflicts:
#	erpnext/controllers/website_list_for_contact.py

* fix: merge conflicts

* fix: sort by creation only

---------

Co-authored-by: Hussain Nagaria <hussainbhaitech@gmail.com>
Co-authored-by: Md Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com>
2025-03-21 16:27:05 +05:30
Smit Vora
4e65b7873d Merge pull request #46638 from frappe/mergify/bp/version-15-hotfix/pr-40556
fix(Payment Entry): get contact details from existing contact (backport #40556)
2025-03-21 15:22:44 +05:30
Smit Vora
7dc23d9733 chore: resolve conflicts #39748 2025-03-21 14:09:21 +05:30
David Arnold
f964178008 fix(Payment Entry): get contact details from existing contact (#40556)
(cherry picked from commit 462204fc65)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
2025-03-21 08:28:59 +00:00
Mihir Kandoi
2bfaf64fff fix: date added to wrong patch
(cherry picked from commit dc45c3b39c)

# Conflicts:
#	erpnext/patches.txt
2025-03-20 18:26:02 +00:00
ruthra kumar
6da00319e6 Merge pull request #46612 from frappe/mergify/bp/version-15-hotfix/pr-46173
feat(projects): add option to hide timesheets for project users (backport #46173)
2025-03-19 16:50:42 +05:30
Marc Ramser
3834d6fbce feat(projects): add option to hide timesheets for project users (#46173)
* feat: add option to hide timesheets for project users

* Added a new "Hide timesheets" checkbox field to Project User doctype that allows to control timesheet visibility for specific users. When enabled, the timesheets section will not be displayed on the project page for that user.

* Update projects.html

(cherry picked from commit f4aba561ce)
2025-03-19 08:05:42 +00:00
33 changed files with 582 additions and 284 deletions

View File

@@ -0,0 +1,30 @@
name: "Auto-label PRs based on title"
on:
pull_request_target:
types: [opened, reopened]
jobs:
add-label-if-prefix-matches:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Check PR title and add label if it matches prefixes
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const title = context.payload.pull_request.title.toLowerCase();
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
// Check if the PR title starts with any of the prefixes
if (prefixes.some(prefix => title.startsWith(prefix))) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['skip-release-notes']
});
}

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.54.5"
__version__ = "15.55.5"
def get_default_company(user=None):

View File

@@ -116,6 +116,7 @@ def identify_is_group(child):
return is_group
@frappe.whitelist()
def get_chart(chart_template, existing_company=None):
chart = {}
if existing_company:

View File

@@ -159,9 +159,6 @@ def get_payment_entries_for_bank_clearance(
as_dict=1,
)
if bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
f"""
select
@@ -183,7 +180,6 @@ def get_payment_entries_for_bank_clearance(
"account": account,
"from": from_date,
"to": to_date,
"bank_account": bank_account,
},
as_dict=1,
)

View File

@@ -576,8 +576,22 @@ class JournalEntry(AccountsController):
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
customer_details = frappe._dict(
frappe.db.get_all(
"Customer Credit Limit",
filters={
"parent": ["in", customers],
"parenttype": ["=", "Customer"],
"company": ["=", self.company],
},
fields=["parent", "bypass_credit_limit_check"],
as_list=True,
)
)
for customer in customers:
check_credit_limit(customer, self.company)
ignore_outstanding_sales_order = bool(customer_details.get(customer))
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
def validate_cheque_info(self):
if self.voucher_type in ["Bank Entry"]:

View File

@@ -7,6 +7,7 @@ from functools import reduce
import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.model.meta import get_field_precision
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
from frappe.utils import cint, comma_or, flt, getdate, nowdate
@@ -37,7 +38,7 @@ from erpnext.accounts.general_ledger import (
make_reverse_gl_entries,
process_gl_map,
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.party import complete_contact_details, get_party_account, set_contact_details
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
@@ -439,6 +440,12 @@ class PaymentEntry(AccountsController):
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
if self.party:
if not self.contact_person:
set_contact_details(
self, party=frappe._dict({"name": self.party}), party_type=self.party_type
)
else:
complete_contact_details(self)
if not self.party_balance:
self.party_balance = get_balance_on(
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
@@ -736,16 +743,39 @@ class PaymentEntry(AccountsController):
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
conversion_rate = frappe.db.get_value(key[2], {"name": key[1]}, "conversion_rate")
base_paid_amount_precision = get_field_precision(
frappe.get_meta("Payment Schedule").get_field("base_paid_amount")
)
base_outstanding_precision = get_field_precision(
frappe.get_meta("Payment Schedule").get_field("base_outstanding")
)
base_paid_amount = flt(
(allocated_amount - discounted_amt) * conversion_rate, base_paid_amount_precision
)
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
if cancel:
frappe.db.sql(
"""
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
base_paid_amount = `base_paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s
outstanding = `outstanding` + %s,
base_outstanding = `base_outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
(
allocated_amount - discounted_amt,
base_paid_amount,
discounted_amt,
allocated_amount,
base_outstanding,
key[1],
key[0],
),
)
else:
if allocated_amount > outstanding:
@@ -761,10 +791,20 @@ class PaymentEntry(AccountsController):
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
base_paid_amount = `base_paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s
outstanding = `outstanding` - %s,
base_outstanding = `base_outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
(
allocated_amount - discounted_amt,
base_paid_amount,
discounted_amt,
allocated_amount,
base_outstanding,
key[1],
key[0],
),
)
def get_allocated_amount_in_transaction_currency(
@@ -2879,7 +2919,7 @@ def get_payment_entry(
pe.party_type = party_type
pe.party = doc.get(scrub(party_type))
pe.contact_person = doc.get("contact_person")
pe.contact_email = doc.get("contact_email")
complete_contact_details(pe)
pe.ensure_supplier_is_not_blocked()
pe.paid_from = party_account if payment_type == "Receive" else bank.account

View File

@@ -24,7 +24,9 @@
"paid_amount",
"discounted_amount",
"column_break_3",
"base_payment_amount"
"base_payment_amount",
"base_outstanding",
"base_paid_amount"
],
"fields": [
{
@@ -155,19 +157,35 @@
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "base_outstanding",
"fieldtype": "Currency",
"label": "Outstanding (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"depends_on": "base_paid_amount",
"fieldname": "base_paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-09-16 13:57:06.382859",
"modified": "2025-03-11 11:06:51.792982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1

View File

@@ -14,6 +14,8 @@ class PaymentSchedule(Document):
if TYPE_CHECKING:
from frappe.types import DF
base_outstanding: DF.Currency
base_paid_amount: DF.Currency
base_payment_amount: DF.Currency
description: DF.SmallText | None
discount: DF.Float

View File

@@ -8,6 +8,8 @@ from frappe import _, qb
from frappe.model.document import Document
from frappe.utils.data import comma_and
from erpnext.stock import get_warehouse_account_map
class RepostAccountingLedger(Document):
# begin: auto-generated types
@@ -97,6 +99,9 @@ class RepostAccountingLedger(Document):
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
elif doc.doctype == "Purchase Receipt":
warehouse_account_map = get_warehouse_account_map(doc.company)
gle_map = doc.get_gl_entries(warehouse_account_map)
else:
gle_map = doc.get_gl_entries()
@@ -177,6 +182,14 @@ def start_repost(account_repost_doc=str) -> None:
doc.force_set_against_expense_account()
doc.make_gl_entries()
elif doc.doctype == "Purchase Receipt":
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
doc.make_gl_entries(from_repost=True)
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)

View File

@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
@@ -204,9 +206,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_06_repost_purchase_receipt(self):
from erpnext.accounts.doctype.account.test_account import create_account
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
another_provisional_account = create_account(
account_name="Another Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
company = frappe.get_doc("Company", self.company)
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
test_cc = company.cost_center
default_expense_account = company.default_expense_account
item = make_item(properties={"is_stock_item": 0})
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles = [
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
]
self.assertEqual(expected_pr_gles, pr_gl_entries)
# change the provisional account
frappe.db.set_value(
"Purchase Receipt Item",
pr.items[0].name,
"provisional_expense_account",
another_provisional_account,
)
repost_doc = frappe.new_doc("Repost Accounting Ledger")
repost_doc.company = self.company
repost_doc.delete_cancelled_entries = True
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
repost_doc.save().submit()
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles_after_repost = [
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
]
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
# teardown
repost_doc.cancel()
repost_doc.delete()
pr.reload()
pr.cancel()
company.enable_provisional_accounting_for_non_stock_items = 0
company.default_provisional_account = None
company.save()
def update_repost_settings():
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
allowed_types = [
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Purchase Receipt",
]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})

View File

@@ -1819,17 +1819,6 @@ class TestSalesInvoice(FrappeTestCase):
for field in expected_gle:
self.assertEqual(expected_gle[field], gle[field])
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self):
# Customer currency = USD

View File

@@ -279,9 +279,7 @@ def get_regional_address_details(party_details, doctype, company):
pass
def set_contact_details(party_details, party, party_type):
party_details.contact_person = get_default_contact(party_type, party.name)
def complete_contact_details(party_details):
if not party_details.contact_person:
party_details.update(
{
@@ -310,6 +308,11 @@ def set_contact_details(party_details, party, party_type):
party_details.update(contact_details)
def set_contact_details(party_details, party, party_type):
party_details.contact_person = get_default_contact(party_type, party.name)
complete_contact_details(party_details)
def set_other_values(party_details, party, party_type):
# copy
if party_type == "Customer":

View File

@@ -517,7 +517,7 @@ class ReceivablePayableReport:
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
ps.description, ps.paid_amount, ps.discounted_amount
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
@@ -540,20 +540,24 @@ class ReceivablePayableReport:
# Deduct that from paid amount pre allocation
row.paid -= flt(payment_terms_details[0].total_advance)
company_currency = frappe.get_value("Company", self.filters.get("company"), "default_currency")
# If single payment terms, no need to split the row
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
self.append_payment_term(row, payment_terms_details[0], original_row)
self.append_payment_term(row, payment_terms_details[0], original_row, company_currency)
return
for d in payment_terms_details:
term = frappe._dict(original_row)
self.append_payment_term(row, d, term)
self.append_payment_term(row, d, term, company_currency)
def append_payment_term(self, row, d, term):
if d.currency == d.party_account_currency:
def append_payment_term(self, row, d, term, company_currency):
invoiced = d.base_payment_amount
paid_amount = d.base_paid_amount
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
invoiced = d.payment_amount
else:
invoiced = d.base_payment_amount
paid_amount = d.paid_amount
row.payment_terms.append(
term.update(
@@ -562,15 +566,15 @@ class ReceivablePayableReport:
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description or d.payment_term,
"paid": d.paid_amount + d.discounted_amount,
"paid": paid_amount + d.discounted_amount,
"credit_note": 0.0,
"outstanding": invoiced - d.paid_amount - d.discounted_amount,
"outstanding": invoiced - paid_amount - d.discounted_amount,
}
)
)
if d.paid_amount:
row["paid"] -= d.paid_amount + d.discounted_amount
if paid_amount:
row["paid"] -= paid_amount + d.discounted_amount
def allocate_closing_to_term(self, row, term, key):
if row[key]:

View File

@@ -145,6 +145,130 @@ def get_asset_categories_for_grouped_by_category(filters):
)
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
finance_book_filter = ""
if filters.get("finance_book"):
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
f"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition} {finance_book_filter}
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.asset_category) as results
group by results.asset_category
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"finance_book": filters.get("finance_book", ""),
},
as_dict=1,
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
@@ -224,130 +348,6 @@ def get_asset_details_for_grouped_by_category(filters):
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
finance_book_filter = ""
if filters.get("finance_book"):
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
f"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition} {finance_book_filter}
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.asset_category) as results
group by results.asset_category
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"finance_book": filters.get("finance_book", ""),
},
as_dict=1,
)
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
@@ -405,7 +405,7 @@ def get_assets_for_grouped_by_asset(filters):
group by a.name
union
SELECT a.name as name,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
0
else
a.opening_accumulated_depreciation

View File

@@ -9,8 +9,8 @@ import frappe
import frappe.defaults
from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.query_builder import AliasedQuery, Criterion, Table
from frappe.query_builder.functions import Count, Sum
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
from frappe.query_builder.functions import Count, Max, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
add_days,
@@ -1974,6 +1974,15 @@ class QueryPaymentLedger:
.select(
ple.against_voucher_no.as_("voucher_no"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
Max(
Case().when(
(
(ple.voucher_no == ple.against_voucher_no)
& (ple.voucher_type == ple.against_voucher_type)
),
(ple.posting_date),
)
).as_("invoice_date"),
)
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_against_voucher_no))
@@ -1981,7 +1990,7 @@ class QueryPaymentLedger:
.where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date))
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
.orderby(ple.posting_date, ple.voucher_no)
.orderby(ple.invoice_date, ple.voucher_no)
.having(qb.Field("amount_in_account_currency") > 0)
.limit(self.limit)
.run()

View File

@@ -443,21 +443,22 @@ class AccountsController(TransactionBase):
)
def validate_party_address_and_contact(self):
party, party_type = None, None
if self.get("customer"):
party, party_type = self.customer, "Customer"
party_type, party = self.get_party()
if not (party_type and party):
return
if party_type == "Customer":
billing_address, shipping_address = (
self.get("customer_address"),
self.get("shipping_address_name"),
)
self.validate_party_address(party, party_type, billing_address, shipping_address)
elif self.get("supplier"):
party, party_type = self.supplier, "Supplier"
elif party_type == "Supplier":
billing_address = self.get("supplier_address")
self.validate_party_address(party, party_type, billing_address)
if party and party_type:
self.validate_party_contact(party, party_type)
self.validate_party_contact(party, party_type)
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
if billing_address or shipping_address:
@@ -2363,6 +2364,9 @@ class AccountsController(TransactionBase):
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
d.base_outstanding = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
)
elif not d.invoice_portion:
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
@@ -2689,12 +2693,17 @@ class AccountsController(TransactionBase):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
if not self.conversion_rate:
throw(_("Conversion rate cannot be 0"))
if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))
if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
frappe.msgprint(
_("Conversion rate is 1.00, but document currency is different from company currency")
)
def check_finance_books(self, item, asset):
if (

View File

@@ -545,7 +545,11 @@ class SubcontractingController(StockController):
def __get_batch_nos_for_bundle(self, qty, key):
available_batches = defaultdict(float)
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "consumed_qty")
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if flt(batch_qty, precision) <= 0:
continue
qty_to_consumed = 0
if qty > 0:
if batch_qty >= qty:

View File

@@ -69,7 +69,7 @@ def get_transaction_list(
filters=None,
limit_start=0,
limit_page_length=20,
order_by="modified",
order_by="creation desc",
custom=False,
):
user = frappe.session.user
@@ -115,7 +115,7 @@ def get_transaction_list(
limit_page_length,
fields="name",
ignore_permissions=ignore_permissions,
order_by="modified desc",
order_by=order_by,
)
if custom:

View File

@@ -2602,6 +2602,109 @@ class TestWorkOrder(FrappeTestCase):
status = frappe.db.get_value("Serial No", row, "status")
self.assertEqual(status, "Consumed")
def test_work_order_valuation_auto_pick(self):
fg_item = "Test FG Item For Non Transfer Item Batch"
rm_item = "Test RM Item For Non Transfer Item Batch"
make_item(fg_item, {"is_stock_item": 1})
make_item(
rm_item,
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TST-BATCH-NTI-.###",
},
)
source_warehouse = "_Test Warehouse - _TC"
wip_warehouse = "Stores - _TC"
finished_goods_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
batches = make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse)
if not frappe.db.get_value("BOM", {"item": fg_item}):
make_bom(item=fg_item, raw_materials=[rm_item])
wo = make_wo_order_test_record(
item=fg_item,
qty=5,
source_warehouse=source_warehouse,
wip_warehouse=wip_warehouse,
fg_warehouse=finished_goods_warehouse,
)
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5))
stock_entry.items[0].batch_no = batches[1]
stock_entry.items[0].use_serial_batch_fields = 1
stock_entry.submit()
stock_entry.reload()
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
original_value = frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
)
original_based_on = frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on")
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", "Expiry")
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 5))
stock_entry.items[0].use_serial_batch_fields = 1
stock_entry.submit()
stock_entry.reload()
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
self.assertEqual(batch_no, batches[1])
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
self.assertEqual(stock_entry.items[1].valuation_rate, 200)
frappe.db.set_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
)
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
batches = []
for qty, rate in ((5, 100), (5, 200)):
stock_entry = make_stock_entry_test_record(
item_code=rm_item,
target=source_warehouse,
qty=qty,
basic_rate=rate,
)
stock_entry.submit()
stock_entry.reload()
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
batch_doc = frappe.get_doc("Batch", batch_no)
# keep early expiry date for the batch having rate 200
days = 10 if rate == 100 else 1
batch_doc.db_set("expiry_date", add_to_date(now(), days=days))
batches.append(batch_no)
stock_entry = make_stock_entry_test_record(
item_code=rm_item,
target=wip_warehouse,
qty=qty,
basic_rate=rate,
)
stock_entry.submit()
stock_entry.reload()
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
batch_doc = frappe.get_doc("Batch", batch_no)
batch_doc.db_set("expiry_date", add_to_date(now(), days=10))
return batches
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)

View File

@@ -397,7 +397,8 @@ erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v15_0.update_query_report
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference #2025-03-18
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices

View File

@@ -0,0 +1,18 @@
import frappe
from frappe.query_builder import DocType
def execute():
invoice_types = ["Sales Invoice", "Purchase Invoice"]
for invoice_type in invoice_types:
invoice = DocType(invoice_type)
invoice_details = frappe.qb.from_(invoice).select(invoice.conversion_rate, invoice.name)
update_payment_schedule(invoice_details)
def update_payment_schedule(invoice_details):
ps = DocType("Payment Schedule")
frappe.qb.update(ps).join(invoice_details).on(ps.parent == invoice_details.name).set(
ps.base_paid_amount, ps.paid_amount * invoice_details.conversion_rate
).set(ps.base_outstanding, ps.outstanding * invoice_details.conversion_rate).run()

View File

@@ -12,6 +12,7 @@
"full_name",
"welcome_email_sent",
"view_attachments",
"hide_timesheets",
"section_break_5",
"project_status"
],
@@ -64,6 +65,13 @@
"in_list_view": 1,
"label": "View attachments"
},
{
"columns": 2,
"default": "0",
"fieldname": "hide_timesheets",
"fieldtype": "Check",
"label": "Hide timesheets"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"

View File

@@ -335,7 +335,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let d = locals[cdt][cdn];
return {
filters: {
docstatus: ("<", 2),
docstatus: ["<", 2],
inspection_type: inspection_type,
reference_name: doc.name,
item_code: d.item_code

View File

@@ -447,22 +447,21 @@ erpnext.sales_common = {
args: { project: this.frm.doc.project },
callback: function (r, rt) {
if (!r.exc) {
$.each(me.frm.doc["items"] || [], function (i, row) {
if (r.message) {
if (r.message) {
$.each(me.frm.doc["items"] || [], function (i, row) {
frappe.model.set_value(
row.doctype,
row.name,
"cost_center",
r.message
);
frappe.msgprint(
__(
"Cost Center For Item with Item Code {0} has been Changed to {1}",
[row.item_name, r.message]
)
);
}
});
});
frappe.msgprint(
__("Cost Center for Item rows has been updated to {0}", [
r.message,
])
);
}
}
},
});

View File

@@ -195,6 +195,7 @@ class DeprecatedBatchNoValuation:
@deprecated
def set_balance_value_for_non_batchwise_valuation_batches(self):
self.last_sle = self.get_last_sle_for_non_batch()
self.set_balance_value_from_sl_entries()
self.set_balance_value_from_bundle()
@@ -242,17 +243,15 @@ class DeprecatedBatchNoValuation:
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
last_sle = self.get_last_sle_for_non_batch()
for d in batch_data:
if self.available_qty.get(d.batch_no):
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
self.non_batchwise_balance_value[d.batch_no] += flt(self.last_sle.stock_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(self.last_sle.qty_after_transaction)
def get_last_sle_for_non_batch(self):
from erpnext.stock.utils import get_combine_datetime
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if not self.sle.creation:
@@ -267,8 +266,6 @@ class DeprecatedBatchNoValuation:
query = (
frappe.qb.from_(sle)
.inner_join(batch)
.on(sle.batch_no == batch.name)
.select(
sle.stock_value,
sle.qty_after_transaction,
@@ -276,7 +273,6 @@ class DeprecatedBatchNoValuation:
.where(
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isnotnull())
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
@@ -289,60 +285,8 @@ class DeprecatedBatchNoValuation:
query = query.where(sle.name != self.sle.name)
data = query.run(as_dict=True)
return data[0] if data else {}
@deprecated
def get_last_sle_for_sabb_no_batchwise_valuation(self):
sabb = frappe.qb.DocType("Serial and Batch Bundle")
sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
batch = frappe.qb.DocType("Batch")
posting_datetime = CombineDatetime(self.sle.posting_date, self.sle.posting_time)
timestamp_condition = CombineDatetime(sabb.posting_date, sabb.posting_time) < posting_datetime
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(sabb.posting_date, sabb.posting_time) == posting_datetime
) & (sabb.creation < self.sle.creation)
query = (
frappe.qb.from_(sabb)
.inner_join(sabb_entry)
.on(sabb.name == sabb_entry.parent)
.inner_join(batch)
.on(sabb_entry.batch_no == batch.name)
.select(sabb.name)
.where(
(sabb.item_code == self.sle.item_code)
& (sabb.warehouse == self.sle.warehouse)
& (sabb_entry.batch_no.isnotnull())
& (sabb.is_cancelled == 0)
& (sabb.docstatus == 1)
)
.where(timestamp_condition)
.orderby(sabb.posting_date, order=Order.desc)
.orderby(sabb.posting_time, order=Order.desc)
.orderby(sabb.creation, order=Order.desc)
.limit(1)
)
if self.sle.voucher_detail_no:
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
query = query.where(sabb.voucher_type != "Pick List")
data = query.run(as_dict=True)
if not data:
return {}
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"serial_and_batch_bundle": data[0].name},
["stock_value", "qty_after_transaction"],
as_dict=1,
)
return sle if sle else {}
return data[0] if data else frappe._dict()
@deprecated
def set_balance_value_from_bundle(self) -> None:
@@ -393,10 +337,9 @@ class DeprecatedBatchNoValuation:
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
last_sle = self.get_last_sle_for_sabb_no_batchwise_valuation()
if not last_sle:
if not self.last_sle:
return
for batch_no in self.available_qty:
self.non_batchwise_balance_value[batch_no] = flt(last_sle.stock_value)
self.non_batchwise_balance_qty[batch_no] = flt(last_sle.qty_after_transaction)
self.non_batchwise_balance_value[batch_no] = flt(self.last_sle.stock_value)
self.non_batchwise_balance_qty[batch_no] = flt(self.last_sle.qty_after_transaction)

View File

@@ -197,8 +197,19 @@ class QualityInspection(Document):
self.quality_inspection_template = template
self.get_item_specification_details()
def on_update(self):
if (
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
== "Warn"
):
self.update_qc_reference()
def on_submit(self):
self.update_qc_reference()
if (
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
== "Stop"
):
self.update_qc_reference()
def on_cancel(self):
self.ignore_linked_doctypes = "Serial and Batch Bundle"
@@ -206,15 +217,15 @@ class QualityInspection(Document):
self.update_qc_reference()
def on_trash(self):
self.update_qc_reference()
self.update_qc_reference(remove_reference=True)
def validate_readings_status_mandatory(self):
for reading in self.readings:
if not reading.status:
frappe.throw(_("Row #{0}: Status is mandatory").format(reading.idx))
def update_qc_reference(self):
quality_inspection = self.name if self.docstatus == 1 else ""
def update_qc_reference(self, remove_reference=False):
quality_inspection = self.name if self.docstatus < 2 and not remove_reference else ""
if self.reference_type == "Job Card":
if self.reference_name:
@@ -244,7 +255,7 @@ class QualityInspection(Document):
)
)
if self.batch_no and self.docstatus == 1:
if self.batch_no and self.docstatus < 2:
query = query.where(child_doc.batch_no == self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name

View File

@@ -2103,7 +2103,8 @@ def get_auto_batch_nos(kwargs):
filter_zero_near_batches(available_batches, kwargs)
if not kwargs.consider_negative_batches:
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
available_batches = [d for d in available_batches if flt(d.qty, precision) > 0]
if not qty:
return available_batches

View File

@@ -1026,10 +1026,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
};
});
if (me.frm.doc.company && erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
this.frm.add_fetch("company", "stock_adjustment_account", "expense_account");
}
this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () {
if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
return {
@@ -1143,8 +1139,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company))
this.set_default_account("stock_adjustment_account", "expense_account");
this.set_default_account("cost_center", "cost_center");
this.frm.refresh_fields("items");

View File

@@ -1672,7 +1672,7 @@ class StockEntry(StockController):
if self.purpose == "Material Issue":
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
if self.purpose == "Manufacture":
if self.purpose == "Manufacture" or not ret.get("expense_account"):
ret["expense_account"] = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
)

View File

@@ -250,6 +250,7 @@
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@@ -588,7 +589,8 @@
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
"print_hide": 1,
"search_index": 1
},
{
"default": "0",
@@ -606,11 +608,12 @@
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-25 15:58:40.982582",
"modified": "2025-03-26 21:01:58.544797",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
@@ -620,4 +623,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -1212,9 +1212,21 @@ class update_entries_after:
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
if not sle.dependant_sle_voucher_detail_no:
if not sle.dependant_sle_voucher_detail_no or self.is_manufacture_entry_with_sabb(sle):
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
def is_manufacture_entry_with_sabb(self, sle):
if (
self.args.get("sle_id")
and sle.serial_and_batch_bundle
and sle.auto_created_serial_and_batch_bundle
):
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in ["Manufacture", "Repack"]:
return True
return False
def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no):
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)

View File

@@ -56,8 +56,8 @@
{{ empty_state(_("Task")) }}
{% endif %}
<h4 class="my-account-header">{{ _("Timesheets") }}</h4>
{% if doc.timesheets %}
<h4 class="my-account-header">{{ _("Timesheets") }}</h4>
<div class="website-list">
<div class="result">
<div class="web-list-item transaction-list-item">
@@ -73,8 +73,6 @@
{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
</div>
</div>
{% else %}
{{ empty_state(_("Timesheet")) }}
{% endif %}
{% if doc.attachments %}
@@ -136,4 +134,4 @@
</div>
</div>
</div>
{% endmacro %}
{% endmacro %}

View File

@@ -9,7 +9,7 @@ def get_context(context):
project_user = frappe.db.get_value(
"Project User",
{"parent": frappe.form_dict.project, "user": frappe.session.user},
["user", "view_attachments"],
["user", "view_attachments", "hide_timesheets"],
as_dict=True,
)
if frappe.session.user != "Administrator" and (not project_user or frappe.session.user == "Guest"):
@@ -25,7 +25,8 @@ def get_context(context):
project.name, start=0, item_status="open", search=frappe.form_dict.get("search")
)
project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search"))
if project_user and not project_user.hide_timesheets:
project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search"))
if project_user and project_user.view_attachments:
project.attachments = get_attachments(project.name)