mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-07 03:33:03 +00:00
Compare commits
310 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda447f20a | ||
|
|
7db420cf9f | ||
|
|
5ddfe3ce1d | ||
|
|
ff998572e7 | ||
|
|
f364a41490 | ||
|
|
804edad233 | ||
|
|
e533158a1c | ||
|
|
5bcf642dff | ||
|
|
d7822f493b | ||
|
|
5188745c53 | ||
|
|
006a8c71fd | ||
|
|
ba25d334b1 | ||
|
|
dd1020ccfb | ||
|
|
de2f80811f | ||
|
|
12eaa64572 | ||
|
|
da1dcb8b21 | ||
|
|
9dad08274a | ||
|
|
d8d53bc43e | ||
|
|
8966c956d5 | ||
|
|
a518fc080a | ||
|
|
3b36be214f | ||
|
|
20af226947 | ||
|
|
7f26b1d5a2 | ||
|
|
c332fcdeff | ||
|
|
51adea3b89 | ||
|
|
083402e9a8 | ||
|
|
1116a0fc8e | ||
|
|
b15d7e573e | ||
|
|
9207d79ce6 | ||
|
|
33eb72035b | ||
|
|
2c6099e051 | ||
|
|
5f4aaed5a0 | ||
|
|
94a17b6ec2 | ||
|
|
bb901cab88 | ||
|
|
711e06b537 | ||
|
|
c54b65acc2 | ||
|
|
179b8c7c61 | ||
|
|
b107387dda | ||
|
|
9fcca18bf3 | ||
|
|
b98cce8b9a | ||
|
|
62b7a8d21a | ||
|
|
6b31e54891 | ||
|
|
15ae0196c6 | ||
|
|
a06d705d75 | ||
|
|
9e3e3182f2 | ||
|
|
1393100a62 | ||
|
|
945bdabebb | ||
|
|
f07594b863 | ||
|
|
25a382ec24 | ||
|
|
d7c47e8ea5 | ||
|
|
2627fb10ce | ||
|
|
0790db79fd | ||
|
|
0dad2acfc8 | ||
|
|
193042ef07 | ||
|
|
8208a9ec27 | ||
|
|
fbfc6a6f62 | ||
|
|
e9f9eaa2d0 | ||
|
|
08d40ddb9b | ||
|
|
d60ab92082 | ||
|
|
85a0581145 | ||
|
|
2cac05e56c | ||
|
|
10e4e610db | ||
|
|
9ab80cfd6c | ||
|
|
8f2b83d434 | ||
|
|
7f0ebb37da | ||
|
|
1d42c4a305 | ||
|
|
186173a21e | ||
|
|
96c937bf6a | ||
|
|
52aafc9410 | ||
|
|
3232310c0f | ||
|
|
6397c366fd | ||
|
|
f5ef376486 | ||
|
|
c9eeca22f5 | ||
|
|
b145638eca | ||
|
|
b46fdc2645 | ||
|
|
df779bb7dd | ||
|
|
20f701ff0b | ||
|
|
ca4858318e | ||
|
|
b664781fae | ||
|
|
69463b4798 | ||
|
|
caf0f2cd19 | ||
|
|
1cfb002de8 | ||
|
|
3b686827e9 | ||
|
|
d37b12f385 | ||
|
|
43aeff38aa | ||
|
|
ad13dcd695 | ||
|
|
792f3afa1b | ||
|
|
43dda4a5d2 | ||
|
|
274ce10329 | ||
|
|
caf145e3ca | ||
|
|
05911ad563 | ||
|
|
de937a6bad | ||
|
|
70e6ea6b3f | ||
|
|
70bcfb4748 | ||
|
|
1f1cb338fe | ||
|
|
d5d4b3a7f3 | ||
|
|
5d9be7366b | ||
|
|
79a4b597d4 | ||
|
|
0b0944cc06 | ||
|
|
537c917bfc | ||
|
|
13f393c59a | ||
|
|
54197ff760 | ||
|
|
6f1109226d | ||
|
|
9ce86b135b | ||
|
|
d88feecf46 | ||
|
|
271d0a301f | ||
|
|
9d36166616 | ||
|
|
00407cd0ee | ||
|
|
a04feff264 | ||
|
|
d6f5ac90b1 | ||
|
|
c6c1b00559 | ||
|
|
b86dc294f1 | ||
|
|
949ed59f84 | ||
|
|
558225b027 | ||
|
|
2389fd5145 | ||
|
|
d66a34411c | ||
|
|
1bbbd261cb | ||
|
|
182b9892fc | ||
|
|
015525599f | ||
|
|
3993525bf6 | ||
|
|
8343c92ac5 | ||
|
|
f278120aa0 | ||
|
|
862e6330ac | ||
|
|
bd395e2406 | ||
|
|
4376fbc3ed | ||
|
|
c49ee563fd | ||
|
|
8e0bd976c3 | ||
|
|
d66e8e8597 | ||
|
|
d41fec7d2b | ||
|
|
378821d9e6 | ||
|
|
698e260526 | ||
|
|
fc793f0f25 | ||
|
|
204d1d6a53 | ||
|
|
aee917b790 | ||
|
|
892d3980d3 | ||
|
|
e743d5f66b | ||
|
|
9adb863787 | ||
|
|
150cc5a664 | ||
|
|
b507e63375 | ||
|
|
27a8856dca | ||
|
|
b13e0a6b9f | ||
|
|
04bb4f8415 | ||
|
|
6735ba0ab6 | ||
|
|
0c5c34409e | ||
|
|
9dbc1cc5fb | ||
|
|
7130e5ccfb | ||
|
|
0e42516977 | ||
|
|
48748e7142 | ||
|
|
0bc3bc4851 | ||
|
|
d791108649 | ||
|
|
0d84059fde | ||
|
|
6868da5f6a | ||
|
|
f906568fea | ||
|
|
17dc87a2df | ||
|
|
522597d22b | ||
|
|
e4c2442ba2 | ||
|
|
0d9bc4424c | ||
|
|
35c9375f0d | ||
|
|
f04c73b9c2 | ||
|
|
a766aa0837 | ||
|
|
3248415667 | ||
|
|
4fb12754ce | ||
|
|
503b89a764 | ||
|
|
b88f6c1252 | ||
|
|
e5156e6666 | ||
|
|
8801584c8d | ||
|
|
55ac96ff09 | ||
|
|
3f3fb323cf | ||
|
|
253a067592 | ||
|
|
d8f301f1db | ||
|
|
6d65c967da | ||
|
|
4ac5c7e5e7 | ||
|
|
911014a54e | ||
|
|
694f158fc8 | ||
|
|
27ecd3a21b | ||
|
|
5844aafd12 | ||
|
|
e684807bc7 | ||
|
|
3af506e0a9 | ||
|
|
98ffbfb432 | ||
|
|
6ec33c0098 | ||
|
|
0e1045e855 | ||
|
|
34d3f58149 | ||
|
|
efd34817be | ||
|
|
124ad729ab | ||
|
|
c388b7579c | ||
|
|
a2595350f6 | ||
|
|
eaa297475f | ||
|
|
ac7700dff0 | ||
|
|
7a74dac2c2 | ||
|
|
a3d4d34454 | ||
|
|
443ed5b2ce | ||
|
|
033fa09eb4 | ||
|
|
056cc35379 | ||
|
|
f1a864349e | ||
|
|
24681bd64f | ||
|
|
b0c742b64e | ||
|
|
18282b2632 | ||
|
|
46115d6f71 | ||
|
|
f62905f7a7 | ||
|
|
88facb7523 | ||
|
|
512877ab46 | ||
|
|
c7e6b2356f | ||
|
|
8e2bfc6bcb | ||
|
|
98260b990c | ||
|
|
3be0f00b3b | ||
|
|
f44478db78 | ||
|
|
db8cb70bd8 | ||
|
|
f26b22ee7f | ||
|
|
1e4807f16d | ||
|
|
698d5be840 | ||
|
|
59e46e22d3 | ||
|
|
14b4d147a8 | ||
|
|
2bed1c8336 | ||
|
|
9e3a04136c | ||
|
|
dcc53cea55 | ||
|
|
430d1e8b2e | ||
|
|
36a366d962 | ||
|
|
dda35b8e51 | ||
|
|
4e0d7d88ec | ||
|
|
36b951d018 | ||
|
|
233e0d8049 | ||
|
|
5f467be0c8 | ||
|
|
70d117e858 | ||
|
|
2fe9fa7ef7 | ||
|
|
b4ca51ad4c | ||
|
|
fb5777cf10 | ||
|
|
06c32993ea | ||
|
|
466c6ee3d4 | ||
|
|
a8831351e3 | ||
|
|
d1f9444be7 | ||
|
|
ceaea16d84 | ||
|
|
119f2e7074 | ||
|
|
9cadb89678 | ||
|
|
da6affbba7 | ||
|
|
6293b08540 | ||
|
|
576ce7e882 | ||
|
|
67d7ffa430 | ||
|
|
d90c8a9150 | ||
|
|
15bb1fdb24 | ||
|
|
509c5c4d17 | ||
|
|
cb028b8740 | ||
|
|
c3447c030a | ||
|
|
1d65f995e7 | ||
|
|
3a6d8cd9a1 | ||
|
|
88e664b79f | ||
|
|
090ee7f042 | ||
|
|
b311b6eb7f | ||
|
|
df64d2ef4e | ||
|
|
690a939572 | ||
|
|
a3639b055c | ||
|
|
331ecc1964 | ||
|
|
736b125e14 | ||
|
|
b9b08c35ef | ||
|
|
7e8d2dc0fc | ||
|
|
39af6bd143 | ||
|
|
300f1a860b | ||
|
|
1441ef2532 | ||
|
|
64fd5c0ba9 | ||
|
|
16fe53b8c7 | ||
|
|
af3089b96e | ||
|
|
3cb29fc035 | ||
|
|
8a93057844 | ||
|
|
805549f793 | ||
|
|
6e497f73f1 | ||
|
|
33f090a8c8 | ||
|
|
20b43b4d93 | ||
|
|
5c1f6f0107 | ||
|
|
6f0c67a242 | ||
|
|
609bfaefba | ||
|
|
2546589b4c | ||
|
|
f311a0fc1c | ||
|
|
a1585b3c53 | ||
|
|
05c0bf5d99 | ||
|
|
247a006b5a | ||
|
|
e0cf6393ee | ||
|
|
1f54c272ac | ||
|
|
9a1321ab02 | ||
|
|
6ca1f9bc73 | ||
|
|
b404f00ece | ||
|
|
0f56fbcf6d | ||
|
|
b3b7e62a90 | ||
|
|
35a728d22b | ||
|
|
ac25d3e1c4 | ||
|
|
90b5f0b7bf | ||
|
|
557a05b0ad | ||
|
|
b41ee667b9 | ||
|
|
fbf6d8c9e2 | ||
|
|
167069b823 | ||
|
|
2d72a37d0c | ||
|
|
77ad224cf6 | ||
|
|
b5b55fad6c | ||
|
|
7cde990d69 | ||
|
|
cedf577b4c | ||
|
|
8e02dcfcaa | ||
|
|
399ed331e3 | ||
|
|
59236bc5bf | ||
|
|
1089cdf213 | ||
|
|
df4f4d9a31 | ||
|
|
f8bbcab3a5 | ||
|
|
369a692af9 | ||
|
|
8cc92e9ca0 | ||
|
|
eebf6cf877 | ||
|
|
ca94ad3a24 | ||
|
|
1a8bf0cf3d | ||
|
|
19604e91c6 | ||
|
|
4f40939c15 | ||
|
|
811953b9b0 | ||
|
|
6748780591 | ||
|
|
69a8e0dfac | ||
|
|
c734373c9f |
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal 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']
|
||||
});
|
||||
}
|
||||
7
.github/workflows/linters.yml
vendored
7
.github/workflows/linters.yml
vendored
@@ -9,15 +9,16 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.83.0"
|
||||
__version__ = "14.88.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.db.get_value("Account", name, "company", as_dict=True)
|
||||
if not account:
|
||||
return
|
||||
@@ -420,7 +421,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
if old_name and not from_descendant:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -514,3 +516,27 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -3,4 +3,14 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,9 +73,14 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -479,6 +484,34 @@
|
||||
"fieldname": "ignore_is_opening_check_for_reporting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Is Opening check for reporting"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -486,7 +519,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified": "2025-05-05 12:29:38.302027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -515,4 +548,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,11 @@ class AccountsSettings(Document):
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -46,9 +46,6 @@ class BankClearance(Document):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -70,7 +67,6 @@ class BankClearance(Document):
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
"to": self.to_date,
|
||||
"bank_account": self.bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -93,7 +89,7 @@ class BankClearance(Document):
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_disbursement.disbursement_account == self.account)
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
)
|
||||
@@ -121,7 +117,7 @@ class BankClearance(Document):
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_repayment.payment_account == self.account)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import set_name_from_naming_options
|
||||
from frappe.utils import flt, fmt_money
|
||||
from frappe.utils import flt, fmt_money, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -405,7 +405,7 @@ def rename_temporarily_named_docs(doctype):
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -34,7 +34,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
finance_book: frm.doc.finance_book,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -433,8 +433,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"]:
|
||||
|
||||
@@ -319,7 +319,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
"from_date": frm.doc.posting_date,
|
||||
"to_date": moment(frm.doc.modified).format('YYYY-MM-DD'),
|
||||
"company": frm.doc.company,
|
||||
"group_by": "",
|
||||
"categorize_by": "",
|
||||
"show_cancelled_entries": frm.doc.docstatus === 2
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -350,15 +350,25 @@ class PaymentEntry(AccountsController):
|
||||
self.set(self.party_account_field, party_account)
|
||||
self.party_account = party_account
|
||||
|
||||
if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance):
|
||||
if self.paid_from and (
|
||||
not self.paid_from_account_currency
|
||||
or not self.paid_from_account_balance
|
||||
or not self.paid_from_account_type
|
||||
):
|
||||
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
|
||||
self.paid_from_account_currency = acc.account_currency
|
||||
self.paid_from_account_balance = acc.account_balance
|
||||
self.paid_from_account_type = acc.account_type
|
||||
|
||||
if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance):
|
||||
if self.paid_to and (
|
||||
not self.paid_to_account_currency
|
||||
or not self.paid_to_account_balance
|
||||
or not self.paid_to_account_type
|
||||
):
|
||||
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
|
||||
self.paid_to_account_currency = acc.account_currency
|
||||
self.paid_to_account_balance = acc.account_balance
|
||||
self.paid_to_account_type = acc.account_type
|
||||
|
||||
self.party_account_currency = (
|
||||
self.paid_from_account_currency
|
||||
@@ -1519,7 +1529,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
@@ -1580,7 +1590,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
@@ -1631,7 +1641,7 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
|
||||
@@ -29,7 +29,7 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -32,8 +32,13 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Account Closing Balance",
|
||||
)
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
|
||||
@@ -1623,6 +1623,5 @@
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"posting_date",
|
||||
"company",
|
||||
"account",
|
||||
"group_by",
|
||||
"categorize_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
@@ -172,14 +172,6 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"default": "Group by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "group_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group By",
|
||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "currency",
|
||||
@@ -395,10 +387,18 @@
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
},
|
||||
{
|
||||
"default": "Categorize by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "categorize_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
@@ -433,4 +433,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"categorize_by": doc.categorize_by,
|
||||
"currency": doc.currency,
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
|
||||
@@ -1124,6 +1124,30 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
|
||||
@@ -1852,7 +1852,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
@@ -1984,6 +1984,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dr_note.save)
|
||||
|
||||
def test_apply_discount_on_grand_total(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
the grand total is calculated correctly without any rounding errors
|
||||
"""
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 21.39,
|
||||
},
|
||||
)
|
||||
invoice.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 15.5,
|
||||
},
|
||||
)
|
||||
|
||||
# the grand total here will be 255.71
|
||||
invoice.disable_rounded_total = 1
|
||||
# apply discount on grand total to adjust the grand total to 255
|
||||
invoice.discount_amount = 0.71
|
||||
invoice.save()
|
||||
|
||||
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 255)
|
||||
|
||||
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
where the tax is calculated on previous row total, the grand total is calculated correctly
|
||||
"""
|
||||
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.extend(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# the total here will be 340, so applying 40 discount
|
||||
invoice.discount_amount = 40
|
||||
invoice.save()
|
||||
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
|
||||
@@ -6,6 +6,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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -77,6 +79,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()
|
||||
|
||||
@@ -155,6 +160,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)
|
||||
|
||||
@@ -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):
|
||||
@@ -202,9 +204,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})
|
||||
|
||||
@@ -91,8 +91,8 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
|
||||
@@ -416,9 +416,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500.01)
|
||||
self.assertEqual(si.grand_total, 1500.01)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.rounding_adjustment, 0)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
@@ -1773,17 +1773,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
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
|
||||
|
||||
@@ -3298,6 +3287,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3305,24 +3295,23 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -3871,6 +3860,35 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_create_return_invoice_for_self_update(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
invoice = create_sales_invoice()
|
||||
|
||||
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
|
||||
payment_entry.reference_no = "test001"
|
||||
payment_entry.reference_date = getdate()
|
||||
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
r_invoice = make_return_doc(invoice.doctype, invoice.name)
|
||||
|
||||
r_invoice.update_outstanding_for_self = 0
|
||||
r_invoice.save()
|
||||
|
||||
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
|
||||
|
||||
r_invoice.submit()
|
||||
|
||||
self.assertNotEqual(r_invoice.outstanding_amount, 0)
|
||||
|
||||
invoice.reload()
|
||||
|
||||
self.assertEqual(invoice.outstanding_amount, 0)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -622,6 +622,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
|
||||
@@ -243,17 +243,18 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
pe1 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
pe1.paid_from = "Debtors - _TC"
|
||||
pe1.paid_to = "Cash - _TC"
|
||||
pe1.submit()
|
||||
vouchers.append(pe1)
|
||||
|
||||
# create invoice
|
||||
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
|
||||
@@ -275,6 +276,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
# make another invoice
|
||||
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
|
||||
# TDS should be calculated
|
||||
|
||||
# this payment should not be considered for TCS calculation as it is outside of fiscal year
|
||||
pe2 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
|
||||
)
|
||||
pe2.paid_from = "Debtors - _TC"
|
||||
pe2.paid_to = "Cash - _TC"
|
||||
pe2.posting_date = add_days(fiscal_year[1], -10)
|
||||
pe2.submit()
|
||||
vouchers.append(pe2)
|
||||
|
||||
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
|
||||
si2.submit()
|
||||
vouchers.append(si2)
|
||||
|
||||
@@ -597,35 +597,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
|
||||
|
||||
def validate_due_date(
|
||||
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
|
||||
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None, doctype=None
|
||||
):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
else:
|
||||
if not template_name:
|
||||
return
|
||||
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
|
||||
|
||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
|
||||
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
|
||||
|
||||
msgprint(
|
||||
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
|
||||
party_type, date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -903,12 +902,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
fields=["name", "is_shipping_address"],
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
|
||||
return shipping_addresses[0].name
|
||||
if len(shipping_addresses) == 1:
|
||||
return shipping_addresses[0].name
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -14,7 +15,10 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_currency_precision,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -49,6 +53,10 @@ class ReceivablePayableReport:
|
||||
self.age_as_on = (
|
||||
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
|
||||
)
|
||||
self.ple_fetch_method = (
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -85,13 +93,7 @@ class ReceivablePayableReport:
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
# Get invoice details like bill_no, due_date etc for all invoices
|
||||
self.get_invoice_details()
|
||||
@@ -105,12 +107,45 @@ class ReceivablePayableReport:
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.prepare_ple_query()
|
||||
self.data = []
|
||||
self.voucher_balance = OrderedDict()
|
||||
|
||||
if self.ple_fetch_method == "Buffered Cursor":
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
|
||||
self.build_data()
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
@@ -131,26 +166,22 @@ class ReceivablePayableReport:
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
def init_voucher_balance(self, ple):
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
self.get_invoices(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
@@ -272,6 +303,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -498,7 +602,7 @@ class ReceivablePayableReport:
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
@@ -764,7 +868,7 @@ class ReceivablePayableReport:
|
||||
index = 4
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
def prepare_ple_query(self):
|
||||
# get all the GL entries filtered by the given filters
|
||||
|
||||
self.prepare_conditions()
|
||||
@@ -812,12 +916,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_entries = query.run(as_dict=True)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@@ -1204,3 +1311,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
**args,
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
@@ -111,7 +112,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
def test_accounts_receivable_with_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -151,11 +152,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, True)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
row = report[1][-1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note,
|
||||
[
|
||||
@@ -168,6 +173,105 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_accounts_receivable_without_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"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()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# 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 = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, False)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"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(qty=2)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [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 = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment[i - 1],
|
||||
[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 = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertFalse(cr_note.update_outstanding_for_self)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[200, 100, 0, 80, 20, self.debit_to],
|
||||
[200, 40, 0, 0, 40, self.debit_to],
|
||||
]
|
||||
|
||||
for i in range(2):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note[i - 1],
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.party_account,
|
||||
],
|
||||
)
|
||||
|
||||
def test_cr_note_flag_to_update_self(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options: ["Asset Category", "Asset"],
|
||||
default: "Asset Category",
|
||||
},
|
||||
{
|
||||
fieldname: "asset_category",
|
||||
label: __("Asset Category"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category",
|
||||
depends_on: "eval: doc.group_by == 'Asset Category'",
|
||||
},
|
||||
{
|
||||
fieldname: "asset",
|
||||
label: __("Asset"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,21 +14,28 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
return get_group_by_asset_category_data(filters)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
return get_group_by_asset_data(filters)
|
||||
|
||||
|
||||
def get_group_by_asset_category_data(filters):
|
||||
data = []
|
||||
|
||||
asset_categories = get_asset_categories(filters)
|
||||
assets = get_assets(filters)
|
||||
asset_categories = get_asset_categories_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_category(filters)
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_category)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
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(
|
||||
@@ -38,17 +45,19 @@ def get_data(filters):
|
||||
if asset["asset_category"] == asset_category.get("asset_category", "")
|
||||
)
|
||||
)
|
||||
|
||||
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.cost_as_on_from_date) - flt(
|
||||
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.cost_as_on_to_date) - flt(
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
@@ -57,52 +66,71 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
condition += " and a.asset_category = %(asset_category)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT asset_category,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by asset_category
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
@@ -114,14 +142,17 @@ def get_asset_categories(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
|
||||
# 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,
|
||||
@@ -130,6 +161,11 @@ def get_assets(filters):
|
||||
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
|
||||
@@ -149,15 +185,22 @@ def get_assets(filters):
|
||||
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.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
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}
|
||||
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
|
||||
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
|
||||
@@ -165,51 +208,272 @@ def get_assets(filters):
|
||||
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 {0}
|
||||
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
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
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"):
|
||||
condition += " and a.name = %(asset)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT a.name,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = f" and a.name = '{filters.get('asset')}'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
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.name as name,
|
||||
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}
|
||||
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 then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
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.name) as results
|
||||
group by results.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
columns = []
|
||||
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset"),
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "cost_as_on_from_date",
|
||||
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "value_as_on_from_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Purchase"),
|
||||
"fieldname": "cost_of_new_purchase",
|
||||
"label": _("Value of New Purchase"),
|
||||
"fieldname": "value_of_new_purchase",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Sold Asset"),
|
||||
"fieldname": "cost_of_sold_asset",
|
||||
"label": _("Value of Sold Asset"),
|
||||
"fieldname": "value_of_sold_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Scrapped Asset"),
|
||||
"fieldname": "cost_of_scrapped_asset",
|
||||
"label": _("Value of Scrapped Asset"),
|
||||
"fieldname": "value_of_scrapped_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
"label": _("Value of New Capitalized Asset"),
|
||||
"fieldname": "value_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Value as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "value_as_on_to_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
@@ -237,6 +501,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
@@ -250,3 +520,5 @@ def get_columns(filters):
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<div style="margin-bottom: 7px;">
|
||||
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %}
|
||||
</div>
|
||||
<h2 class="text-center">{%= __("Bank Reconciliation Statement") %}</h2>
|
||||
<h4 class="text-center">{%= filters.account && (filters.account + ", "+filters.report_date) || "" %} {%= filters.company %}</h4>
|
||||
<hr>
|
||||
@@ -46,4 +43,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -67,5 +67,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">
|
||||
Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
|
||||
{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}
|
||||
</p>
|
||||
|
||||
@@ -78,4 +78,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -49,9 +49,14 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "against_voucher_no",
|
||||
label: __("Against Voucher No"),
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
fieldtype: "Break",
|
||||
},
|
||||
@@ -61,7 +66,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldtype: "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.set_filter_value("party", []);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -107,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
"",
|
||||
{
|
||||
label: __("Group by Voucher"),
|
||||
value: "Group by Voucher",
|
||||
label: __("Categorize by Voucher"),
|
||||
value: "Categorize by Voucher",
|
||||
},
|
||||
{
|
||||
label: __("Group by Voucher (Consolidated)"),
|
||||
value: "Group by Voucher (Consolidated)",
|
||||
label: __("Categorize by Voucher (Consolidated)"),
|
||||
value: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
label: __("Group by Account"),
|
||||
value: "Group by Account",
|
||||
label: __("Categorize by Account"),
|
||||
value: "Categorize by Account",
|
||||
},
|
||||
{
|
||||
label: __("Group by Party"),
|
||||
value: "Group by Party",
|
||||
label: __("Categorize by Party"),
|
||||
value: "Categorize by Party",
|
||||
},
|
||||
],
|
||||
default: "Group by Voucher (Consolidated)",
|
||||
default: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
fieldname: "tax_id",
|
||||
|
||||
@@ -72,13 +72,17 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
||||
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||
filters["categorize_by"] = filters["group_by"]
|
||||
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||
|
||||
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
|
||||
filters.account = frappe.parse_json(filters.get("account"))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
||||
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@@ -172,9 +176,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if filters.get("include_dimensions"):
|
||||
order_by_statement = "order by posting_date, creation"
|
||||
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
if filters.get("categorize_by") == "Categorize by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
@@ -225,6 +229,9 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("against_voucher_no"):
|
||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
||||
|
||||
if filters.get("ignore_err"):
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
@@ -258,7 +265,7 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
if filters.get("party_type"):
|
||||
@@ -270,7 +277,7 @@ def get_conditions(filters):
|
||||
if not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
@@ -365,13 +372,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
for _acc, acc_dict in gle_map.items():
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher":
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
@@ -380,7 +387,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher":
|
||||
data.append(acc_dict.totals.closing)
|
||||
data.append({})
|
||||
else:
|
||||
@@ -413,9 +420,9 @@ def get_totals_dict():
|
||||
|
||||
|
||||
def group_by_field(group_by):
|
||||
if group_by == "Group by Party":
|
||||
if group_by == "Categorize by Party":
|
||||
return "party"
|
||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
||||
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
|
||||
return "account"
|
||||
else:
|
||||
return "voucher_no"
|
||||
@@ -423,7 +430,7 @@ def group_by_field(group_by):
|
||||
|
||||
def initialize_gle_map(gl_entries, filters):
|
||||
gle_map = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
|
||||
for gle in gl_entries:
|
||||
gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[]))
|
||||
@@ -434,8 +441,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
totals = get_totals_dict()
|
||||
entries = []
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
|
||||
|
||||
if filters.get("show_net_values_in_party_account"):
|
||||
account_type_map = get_account_type_map(filters.get("company"))
|
||||
@@ -506,6 +513,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
keylist.append(gle.get("project"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
@@ -530,17 +538,20 @@ def get_account_type_map(company):
|
||||
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
balance = get_balance(d, balance, "debit", "credit")
|
||||
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
|
||||
d["presentation_currency"] = filters.presentation_currency
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -566,11 +577,8 @@ def get_columns(filters):
|
||||
if filters.get("presentation_currency"):
|
||||
currency = filters["presentation_currency"]
|
||||
else:
|
||||
if filters.get("company"):
|
||||
currency = get_company_currency(filters["company"])
|
||||
else:
|
||||
company = get_default_company()
|
||||
currency = get_company_currency(company)
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
columns = [
|
||||
{
|
||||
@@ -591,19 +599,22 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Debit ({0})").format(currency),
|
||||
"fieldname": "debit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Credit ({0})").format(currency),
|
||||
"fieldname": "credit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Balance ({0})").format(currency),
|
||||
"fieldname": "balance",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
|
||||
@@ -617,10 +628,11 @@ def get_columns(filters):
|
||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
columns.append(
|
||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": True,
|
||||
}
|
||||
)
|
||||
@@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": False,
|
||||
}
|
||||
)
|
||||
@@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
@@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
apply_order_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
|
||||
@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query = query.orderby(ii.item_code)
|
||||
query += f" order by {ii.item_code}"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query = query.orderby(ii.item_group)
|
||||
query += f" order by {ii.item_group}"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
|
||||
return query
|
||||
|
||||
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.orderby(pi.posting_date, pi.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -413,8 +412,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -438,7 +438,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
si.company,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.orderby(si.posting_date, si.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -453,8 +452,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -33,6 +34,7 @@ def execute(filters=None):
|
||||
|
||||
def validate_filters(filters):
|
||||
"""Validate if dates are properly set"""
|
||||
filters = frappe._dict(filters or {})
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
@@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
@@ -119,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
@@ -435,12 +437,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
def get_tax_rate_map(filters):
|
||||
rate_map = frappe.get_all(
|
||||
"Tax Withholding Rate",
|
||||
filters={
|
||||
"from_date": ("<=", filters.get("from_date")),
|
||||
"to_date": (">=", filters.get("to_date")),
|
||||
},
|
||||
fields=["parent", "tax_withholding_rate"],
|
||||
as_list=1,
|
||||
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
|
||||
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
|
||||
)
|
||||
|
||||
return frappe._dict(rate_map)
|
||||
rate_list = frappe._dict()
|
||||
|
||||
for rate in rate_map:
|
||||
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
|
||||
|
||||
return rate_list
|
||||
|
||||
|
||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
||||
# returns the row that matches with the fiscal year from posting date
|
||||
for rate in tax_withholding:
|
||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
||||
return rate.tax_withholding_rate
|
||||
|
||||
return 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_to_date, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -60,6 +60,58 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def test_date_filters_in_multiple_tax_withholding_rules(self):
|
||||
create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1)
|
||||
# insert new rate in same fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
},
|
||||
)
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
company="_Test Company",
|
||||
party_type="Supplier",
|
||||
from_date=fiscal_year[1],
|
||||
to_date=fiscal_year[2],
|
||||
)
|
||||
)[1]
|
||||
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def check_expected_values(self, result, expected_values):
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
|
||||
@@ -79,8 +79,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry",
|
||||
label: __("Period Closing Entry"),
|
||||
fieldname: "with_period_closing_entry_for_opening",
|
||||
label: __("With Period Closing Entry For Opening Balances"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_current_period",
|
||||
label: __("Period Closing Entry For Current Period"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@ def get_data(filters):
|
||||
max_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
|
||||
@@ -274,7 +274,7 @@ def get_opening_balance(
|
||||
):
|
||||
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
|
||||
|
||||
if not flt(filters.with_period_closing_entry):
|
||||
if not flt(filters.with_period_closing_entry_for_opening):
|
||||
if doctype == "Account Closing Balance":
|
||||
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
|
||||
@@ -12,8 +12,8 @@ DEFAULT_FILTERS = {
|
||||
|
||||
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}),
|
||||
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Round, Sum
|
||||
@@ -1788,14 +1789,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
):
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
outstanding_amount = flt(
|
||||
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||
)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
|
||||
ref_doc.outstanding_amount = outstanding_amount
|
||||
frappe.db.set_value(
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
"outstanding_amount",
|
||||
outstanding["outstanding_in_account_currency"] or 0.0,
|
||||
outstanding_amount,
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
@@ -2188,3 +2192,19 @@ def run_ledger_health_checks():
|
||||
doc.general_and_payment_ledger_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
|
||||
def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
match_filters = build_match_conditions(doctype, user, False)
|
||||
criterion = []
|
||||
if match_filters:
|
||||
from frappe import qb
|
||||
|
||||
_dt = qb.DocType(doctype)
|
||||
|
||||
for filter in match_filters:
|
||||
for d, names in filter.items():
|
||||
fieldname = d.lower().replace(" ", "_")
|
||||
criterion.append(_dt[fieldname].isin(names))
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "item_code.image",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
@@ -582,7 +583,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-01-15 17:35:49.226603",
|
||||
"modified": "2025-05-20 00:44:06.229177",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -635,38 +635,44 @@ class Asset(AccountsController):
|
||||
return add_days(self.available_for_use_date, -1)
|
||||
|
||||
# if it returns True, depreciation_amount will not be equal for the first and last rows
|
||||
def check_is_pro_rata(self, row, wdv_or_dd_non_yearly=False):
|
||||
def check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
has_pro_rata = False
|
||||
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = self.get_modified_available_for_use_date(row, wdv_or_dd_non_yearly)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
|
||||
if wdv_or_dd_non_yearly:
|
||||
total_days = get_total_days(row.depreciation_start_date, 12)
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
prev_depreciation_start_date = get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * -1) * asset_doc.number_of_depreciations_booked,
|
||||
)
|
||||
)
|
||||
from_date = asset_doc.available_for_use_date
|
||||
days = date_diff(prev_depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation)
|
||||
else:
|
||||
# if frequency_of_depreciation is 12 months, total_days = 365
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
|
||||
if days <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""Error: This asset already has {0} depreciation periods booked.
|
||||
The `depreciation start` date must be at least {1} periods after the `available for use` date.
|
||||
Please correct the dates accordingly."""
|
||||
).format(
|
||||
asset_doc.number_of_depreciations_booked,
|
||||
asset_doc.number_of_depreciations_booked,
|
||||
)
|
||||
)
|
||||
|
||||
if days < total_days:
|
||||
has_pro_rata = True
|
||||
|
||||
return has_pro_rata
|
||||
|
||||
def get_modified_available_for_use_date(self, row, wdv_or_dd_non_yearly=False):
|
||||
if wdv_or_dd_non_yearly:
|
||||
return add_months(
|
||||
self.available_for_use_date,
|
||||
(self.number_of_depreciations_booked * 12),
|
||||
)
|
||||
else:
|
||||
return add_months(
|
||||
self.available_for_use_date,
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
def validate_asset_finance_books(self, row):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
frappe.throw(
|
||||
@@ -1290,6 +1296,28 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
return books
|
||||
|
||||
|
||||
def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
"""
|
||||
if Asset has opening booked depreciations = 9,
|
||||
available for use date = 17-07-2023,
|
||||
depreciation start date = 30-04-2024
|
||||
then from date should be 01-04-2024
|
||||
"""
|
||||
if asset_doc.number_of_depreciations_booked > 0:
|
||||
from_date = add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation) - 1,
|
||||
)
|
||||
if is_last_day_of_the_month(row.depreciation_start_date):
|
||||
return add_days(get_last_day(from_date), 1)
|
||||
|
||||
# get from date when depreciation start date is not last day of the month
|
||||
months_difference = month_diff(row.depreciation_start_date, from_date) - 1
|
||||
return add_days(add_months(row.depreciation_start_date, -1 * months_difference), 1)
|
||||
else:
|
||||
return asset_doc.available_for_use_date
|
||||
|
||||
|
||||
def get_asset_account(account_name, asset=None, asset_category=None, company=None):
|
||||
account = None
|
||||
if asset:
|
||||
|
||||
@@ -1451,7 +1451,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
affects `value_after_depreciation`
|
||||
"""
|
||||
|
||||
asset = create_asset(calculate_depreciation=1)
|
||||
asset = create_asset(calculate_depreciation=1, depreciation_start_date="2021-12-31")
|
||||
asset.opening_accumulated_depreciation = 2000
|
||||
asset.number_of_depreciations_booked = 1
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: cstr(item.warehouse),
|
||||
qty: flt(item.stock_qty),
|
||||
qty: -1 * flt(item.stock_qty),
|
||||
serial_no: item.serial_no,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
|
||||
@@ -69,6 +69,7 @@ class AssetCapitalization(StockController):
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -82,6 +83,7 @@ class AssetCapitalization(StockController):
|
||||
self.cancel_target_asset()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
|
||||
@@ -94,9 +94,6 @@
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<div style="margin-bottom: 7px;" class="text-center">
|
||||
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %}
|
||||
</div>
|
||||
<h2 class="text-center">{%= __(report.report_name) %}</h2>
|
||||
<h4 class="text-center">{%= filters.item %} </h4>
|
||||
|
||||
@@ -124,9 +121,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 class="text-center"> Analysis Chart </h4>
|
||||
<h4 class="text-center">{%= __("Analysis Chart") %}</h4>
|
||||
<div id="chart_div"></div>
|
||||
|
||||
|
||||
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ label: __("Group by Supplier"), value: "Group by Supplier" },
|
||||
{ label: __("Group by Item"), value: "Group by Item" },
|
||||
{ label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
|
||||
{ label: __("Categorize by Item"), value: "Categorize by Item" },
|
||||
],
|
||||
default: __("Group by Supplier"),
|
||||
default: __("Categorize by Supplier"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
|
||||
@@ -15,6 +15,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
return [], []
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
supplier_quotation_data = get_data(filters)
|
||||
|
||||
@@ -24,6 +26,12 @@ def execute(filters=None):
|
||||
return columns, data, message, chart_data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||
filters["categorize_by"] = filters["group_by"]
|
||||
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
sq = frappe.qb.DocType("Supplier Quotation")
|
||||
sq_item = frappe.qb.DocType("Supplier Quotation Item")
|
||||
@@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters):
|
||||
group_wise_map = defaultdict(list)
|
||||
supplier_qty_price_map = {}
|
||||
|
||||
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
|
||||
group_by_field = (
|
||||
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
|
||||
)
|
||||
company_currency = frappe.db.get_default("currency")
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
@@ -274,7 +284,7 @@ def get_columns(filters):
|
||||
},
|
||||
]
|
||||
|
||||
if filters.get("group_by") == "Group by Item":
|
||||
if filters.get("categorize_by") == "Categorize by Item":
|
||||
group_by_columns.reverse()
|
||||
|
||||
columns[0:0] = group_by_columns # add positioned group by columns to the report
|
||||
|
||||
@@ -20,6 +20,9 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
if d.get("is_free_item"):
|
||||
continue
|
||||
|
||||
# get last purchase details
|
||||
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
|
||||
|
||||
|
||||
@@ -153,6 +153,48 @@ class AccountsController(TransactionBase):
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def validate_against_voucher_outstanding(self):
|
||||
from frappe.model.meta import get_meta
|
||||
|
||||
if not get_meta(self.doctype).has_field("outstanding_amount"):
|
||||
return
|
||||
|
||||
if self.get("is_return") and self.return_against and not self.get("is_pos"):
|
||||
against_voucher_outstanding = frappe.get_value(
|
||||
self.doctype, self.return_against, "outstanding_amount"
|
||||
)
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
|
||||
msg = ""
|
||||
if self.get("update_outstanding_for_self"):
|
||||
msg = (
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
|
||||
"uncheck '{2}' checkbox. <br><br>Or"
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
)
|
||||
|
||||
elif not self.update_outstanding_for_self and (
|
||||
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
|
||||
):
|
||||
self.update_outstanding_for_self = 1
|
||||
msg = (
|
||||
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
|
||||
).format(
|
||||
against_voucher_outstanding,
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
flt(abs(self.outstanding_amount)),
|
||||
)
|
||||
|
||||
if msg:
|
||||
msg += " you can use {} tool to reconcile against {} later.".format(
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
)
|
||||
frappe.msgprint(_(msg))
|
||||
|
||||
def validate(self):
|
||||
if not self.get("is_return") and not self.get("is_debit_note"):
|
||||
self.validate_qty_is_not_zero()
|
||||
@@ -178,6 +220,7 @@ class AccountsController(TransactionBase):
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
self.validate_against_voucher_outstanding()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
self.calculate_taxes_and_totals()
|
||||
@@ -209,20 +252,6 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||
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)):
|
||||
self.set_advances()
|
||||
@@ -655,7 +684,9 @@ class AccountsController(TransactionBase):
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
None,
|
||||
self.payment_terms_template,
|
||||
self.doctype,
|
||||
)
|
||||
elif self.doctype == "Purchase Invoice":
|
||||
validate_due_date(
|
||||
@@ -666,6 +697,7 @@ class AccountsController(TransactionBase):
|
||||
self.company,
|
||||
self.bill_date,
|
||||
self.payment_terms_template,
|
||||
self.doctype,
|
||||
)
|
||||
|
||||
def set_price_list_currency(self, buying_or_selling):
|
||||
@@ -1710,69 +1742,50 @@ class AccountsController(TransactionBase):
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
role_allowed_to_overbill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
)
|
||||
|
||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
overbilled_items.append(row)
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_over_bill
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
@@ -1788,55 +1801,88 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
Sales/Purchase Invoice Items
|
||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
||||
that are submitted OR not submitted but are under current invoice
|
||||
"""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
||||
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
item_doctype = frappe.qb.DocType(item.doctype)
|
||||
ref_wise_billed_amount = {}
|
||||
precision = self.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
for item in self.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(Sum(based_on_field))
|
||||
.where(join_field == item.get(item_ref_dn))
|
||||
.where(
|
||||
Criterion.any(
|
||||
[ # select all items from other invoices OR current invoices
|
||||
Criterion.all(
|
||||
[ # for selecting items from other invoices
|
||||
item_doctype.docstatus == 1,
|
||||
item_doctype.parent != self.name,
|
||||
]
|
||||
),
|
||||
Criterion.all(
|
||||
[ # for selecting items from current invoice, that are linked to same reference
|
||||
item_doctype.docstatus == 0,
|
||||
item_doctype.parent == self.name,
|
||||
item_doctype.name != item.name,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
return result[0][0] if result else 0
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
|
||||
).format(item.item_code, item.idx, max_allowed_amt)
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items, precision):
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
@@ -2100,7 +2146,9 @@ class AccountsController(TransactionBase):
|
||||
and automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
if self.get("payment_terms_template"):
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
elif self.get("payment_terms_template"):
|
||||
@@ -2141,7 +2189,9 @@ class AccountsController(TransactionBase):
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self):
|
||||
@@ -2179,7 +2229,9 @@ class AccountsController(TransactionBase):
|
||||
def linked_order_has_payment_schedule(self, po_or_so):
|
||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||
|
||||
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
|
||||
def fetch_payment_terms_from_order(
|
||||
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
):
|
||||
"""
|
||||
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
||||
"""
|
||||
@@ -2195,12 +2247,25 @@ class AccountsController(TransactionBase):
|
||||
"invoice_portion": schedule.invoice_portion,
|
||||
"mode_of_payment": schedule.mode_of_payment,
|
||||
"description": schedule.description,
|
||||
"payment_amount": schedule.payment_amount,
|
||||
"base_payment_amount": schedule.base_payment_amount,
|
||||
"outstanding": schedule.outstanding,
|
||||
"paid_amount": schedule.paid_amount,
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
)
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||
else:
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
schedule.base_payment_amount * self.get("conversion_rate"),
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
|
||||
if schedule.discount_type == "Percentage":
|
||||
payment_schedule["discount_type"] = schedule.discount_type
|
||||
payment_schedule["discount"] = schedule.discount
|
||||
@@ -2459,12 +2524,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 (
|
||||
|
||||
@@ -368,7 +368,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
company = frappe.db.get_value("Delivery Note", source_name, "company")
|
||||
company = frappe.db.get_value(doctype, source_name, "company")
|
||||
default_warehouse_for_sales_return = frappe.get_cached_value(
|
||||
"Company", company, "default_warehouse_for_sales_return"
|
||||
)
|
||||
|
||||
@@ -801,7 +801,7 @@ class StockController(AccountsController):
|
||||
child_tab.item_code,
|
||||
child_tab.qty,
|
||||
)
|
||||
.where(parent_tab.docstatus < 2)
|
||||
.where(parent_tab.docstatus == 1)
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
@@ -1027,12 +1027,7 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -373,9 +373,7 @@ class calculate_taxes_and_totals:
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment")
|
||||
if not rounding_adjustment_computed:
|
||||
self.doc.rounding_adjustment = 0
|
||||
self.grand_total_diff = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict(
|
||||
@@ -440,9 +438,8 @@ class calculate_taxes_and_totals:
|
||||
and self.discount_amount_applied
|
||||
and self.doc.discount_amount
|
||||
and self.doc.apply_discount_on == "Grand Total"
|
||||
and not rounding_adjustment_computed
|
||||
):
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.grand_total_diff = flt(
|
||||
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"),
|
||||
)
|
||||
@@ -528,11 +525,11 @@ class calculate_taxes_and_totals:
|
||||
return self.adjust_grand_total_for_inclusive_tax()
|
||||
|
||||
def adjust_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
# if any inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
non_inclusive_tax_amount = sum(
|
||||
flt(d.tax_amount_after_discount_amount)
|
||||
self.get_tax_amount_if_for_valuation_or_deduction(d.tax_amount_after_discount_amount, d)
|
||||
for d in self.doc.get("taxes")
|
||||
if not d.included_in_print_rate
|
||||
)
|
||||
@@ -549,27 +546,23 @@ class calculate_taxes_and_totals:
|
||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.doc.grand_total_diff = diff
|
||||
else:
|
||||
self.doc.grand_total_diff = 0
|
||||
self.grand_total_diff = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
||||
self.doc.get("grand_total_diff")
|
||||
)
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
||||
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
self.doc.total_taxes_and_charges = 0.0
|
||||
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges"])
|
||||
|
||||
if self.doc.doctype in [
|
||||
"Quotation",
|
||||
@@ -619,7 +612,9 @@ class calculate_taxes_and_totals:
|
||||
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
self.doc.rounded_total = 0
|
||||
self.doc.base_rounded_total = 0
|
||||
self.doc.rounding_adjustment = 0
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
@@ -663,33 +658,29 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
total_for_discount_amount = self.get_total_for_discount_amount()
|
||||
taxes = self.doc.get("taxes")
|
||||
net_total = 0
|
||||
expected_net_total = 0
|
||||
|
||||
if total_for_discount_amount:
|
||||
# calculate item amount after Discount Amount
|
||||
for i, item in enumerate(self._items):
|
||||
for item in self._items:
|
||||
distributed_amount = (
|
||||
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
||||
)
|
||||
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount"))
|
||||
adjusted_net_amount = item.net_amount - distributed_amount
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
|
||||
net_total += item.net_amount
|
||||
|
||||
# discount amount rounding loss adjustment if no taxes
|
||||
if (
|
||||
self.doc.apply_discount_on == "Net Total"
|
||||
or not taxes
|
||||
or total_for_discount_amount == self.doc.net_total
|
||||
) and i == len(self._items) - 1:
|
||||
discount_amount_loss = flt(
|
||||
self.doc.net_total - net_total - self.doc.discount_amount,
|
||||
self.doc.precision("net_total"),
|
||||
)
|
||||
|
||||
# discount amount rounding adjustment
|
||||
if rounding_difference := flt(
|
||||
expected_net_total - net_total, self.doc.precision("net_total")
|
||||
):
|
||||
item.net_amount = flt(
|
||||
item.net_amount + discount_amount_loss, item.precision("net_amount")
|
||||
item.net_amount + rounding_difference, item.precision("net_amount")
|
||||
)
|
||||
net_total += rounding_difference
|
||||
|
||||
item.net_rate = (
|
||||
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
|
||||
@@ -705,20 +696,44 @@ class calculate_taxes_and_totals:
|
||||
def get_total_for_discount_amount(self):
|
||||
if self.doc.apply_discount_on == "Net Total":
|
||||
return self.doc.net_total
|
||||
else:
|
||||
actual_taxes_dict = {}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax)
|
||||
actual_taxes_dict.setdefault(tax.idx, tax_amount)
|
||||
elif tax.row_id in actual_taxes_dict:
|
||||
actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100
|
||||
actual_taxes_dict.setdefault(tax.idx, actual_tax_amount)
|
||||
total_actual_tax = 0
|
||||
actual_taxes_dict = {}
|
||||
|
||||
return flt(
|
||||
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total")
|
||||
def update_actual_tax_dict(tax, tax_amount):
|
||||
nonlocal total_actual_tax
|
||||
|
||||
if tax.get("add_deduct_tax") == "Deduct":
|
||||
tax_amount *= -1
|
||||
|
||||
if tax.get("category") != "Valuation":
|
||||
total_actual_tax += tax_amount
|
||||
|
||||
actual_taxes_dict[int(tax.idx)] = {
|
||||
"tax_amount": tax_amount,
|
||||
"cumulative_tax_amount": total_actual_tax,
|
||||
}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
update_actual_tax_dict(tax, tax.tax_amount)
|
||||
continue
|
||||
|
||||
if not tax.row_id:
|
||||
continue
|
||||
|
||||
base_row = actual_taxes_dict.get(int(tax.row_id))
|
||||
if not base_row:
|
||||
continue
|
||||
|
||||
base_tax_amount = (
|
||||
base_row["tax_amount"]
|
||||
if tax.charge_type == "On Previous Row Amount"
|
||||
else base_row["cumulative_tax_amount"]
|
||||
)
|
||||
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
|
||||
|
||||
return self.doc.grand_total - total_actual_tax
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if not self.doc.docstatus.is_cancelled():
|
||||
@@ -780,9 +795,12 @@ class calculate_taxes_and_totals:
|
||||
if (
|
||||
self.doc.is_return
|
||||
and self.doc.return_against
|
||||
and not self.doc.update_outstanding_for_self
|
||||
and not self.doc.get("is_pos")
|
||||
or self.is_internal_invoice()
|
||||
):
|
||||
# Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
|
||||
self.doc.outstanding_amount = 0
|
||||
return
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
||||
|
||||
@@ -29,4 +29,10 @@ frappe.ui.form.on("Contract", {
|
||||
});
|
||||
}
|
||||
},
|
||||
party_name: function (frm) {
|
||||
let field = frm.doc.party_type.toLowerCase() + "_name";
|
||||
frappe.db.get_value(frm.doc.party_type, frm.doc.party_name, field, (r) => {
|
||||
frm.set_value("party_full_name", r[field]);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"party_user",
|
||||
"status",
|
||||
"fulfilment_status",
|
||||
"party_full_name",
|
||||
"sb_terms",
|
||||
"start_date",
|
||||
"cb_date",
|
||||
@@ -244,11 +245,18 @@
|
||||
"fieldname": "authorised_by_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Authorised By"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_full_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Full Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 11:15:58.385521",
|
||||
"modified": "2025-05-23 13:54:03.346537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract",
|
||||
@@ -315,9 +323,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,17 @@ class Contract(Document):
|
||||
self.name = _(name)
|
||||
|
||||
def validate(self):
|
||||
self.set_missing_values()
|
||||
self.validate_dates()
|
||||
self.update_contract_status()
|
||||
self.update_fulfilment_status()
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.party_full_name:
|
||||
field = self.party_type.lower() + "_name"
|
||||
if res := frappe.db.get_value(self.party_type, self.party_name, field):
|
||||
self.party_full_name = res
|
||||
|
||||
def before_submit(self):
|
||||
self.signed_by_company = frappe.session.user
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
|
||||
app_description = """ERP made simple"""
|
||||
app_icon = "fa fa-th"
|
||||
app_color = "#e74c3c"
|
||||
app_email = "info@erpnext.com"
|
||||
app_email = "hello@frappe.io"
|
||||
app_license = "GNU General Public License (v3)"
|
||||
source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
|
||||
default_mail_footer = """
|
||||
<span>
|
||||
Sent via
|
||||
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
|
||||
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
|
||||
ERPNext
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import deque
|
||||
from operator import itemgetter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, cstr, flt, today
|
||||
@@ -554,9 +554,16 @@ class BOM(WebsiteGenerator):
|
||||
def check_recursion(self, bom_list=None):
|
||||
"""Check whether recursion occurs in any bom"""
|
||||
|
||||
def _throw_error(bom_name):
|
||||
def _throw_error(bom_name, production_item=None):
|
||||
msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name)
|
||||
if production_item and bom_name != self.name:
|
||||
msg += "<br><br>"
|
||||
msg += _(
|
||||
"Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material."
|
||||
).format(bold(production_item))
|
||||
|
||||
frappe.throw(
|
||||
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
|
||||
msg,
|
||||
exc=BOMRecursionError,
|
||||
)
|
||||
|
||||
@@ -573,7 +580,7 @@ class BOM(WebsiteGenerator):
|
||||
if self.item == item.item_code and item.bom_no:
|
||||
# Same item but with different BOM should not be allowed.
|
||||
# Same item can appear recursively once as long as it doesn't have BOM.
|
||||
_throw_error(item.bom_no)
|
||||
_throw_error(item.bom_no, self.item)
|
||||
|
||||
if self.name in {d.bom_no for d in self.items}:
|
||||
_throw_error(self.name)
|
||||
|
||||
@@ -1048,6 +1048,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus < 2)
|
||||
@@ -1115,6 +1116,7 @@ def get_subitems(
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_no)
|
||||
@@ -1228,6 +1230,7 @@ def get_material_request_items(
|
||||
"sales_order": sales_order,
|
||||
"description": row.get("description"),
|
||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||
"main_bom_item": row.get("main_bom_item"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1757,6 +1760,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
|
||||
@@ -92,7 +92,7 @@ class WorkOrder(Document):
|
||||
if self.source_warehouse:
|
||||
self.set_warehouses()
|
||||
|
||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
|
||||
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
|
||||
@@ -1322,20 +1322,20 @@ def stop_unstop(work_order, status):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def query_sales_order(production_item):
|
||||
out = frappe.db.sql_list(
|
||||
"""
|
||||
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
|
||||
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
|
||||
union
|
||||
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
|
||||
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
|
||||
""",
|
||||
(production_item, production_item),
|
||||
def query_sales_order(production_item: str) -> list[str]:
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
filters=[
|
||||
["Sales Order", "docstatus", "=", 1],
|
||||
],
|
||||
or_filters=[
|
||||
["Sales Order Item", "item_code", "=", production_item],
|
||||
["Packed Item", "item_code", "=", production_item],
|
||||
],
|
||||
pluck="name",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_job_card(work_order, operations):
|
||||
|
||||
@@ -23,6 +23,7 @@ def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
_("BOM Qty") + ":Float:160",
|
||||
_("BOM UoM") + "::160",
|
||||
@@ -73,11 +74,12 @@ def get_bom_stock(filters):
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
|
||||
@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||
expected_data.append(
|
||||
[
|
||||
item.item_code,
|
||||
item.item_name,
|
||||
item.description,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
|
||||
@@ -371,4 +371,9 @@ erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
|
||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
erpnext.patches.v14_0.rename_group_by_to_categorize_by
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||
erpnext.patches.v14_0.set_update_price_list_based_on
|
||||
erpnext.patches.v14_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
|
||||
20
erpnext/patches/v14_0/rename_group_by_to_categorize_by.py
Normal file
20
erpnext/patches/v14_0/rename_group_by_to_categorize_by.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
rename_field("Process Statement Of Accounts", "group_by", "categorize_by")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE
|
||||
`tabProcess Statement Of Accounts`
|
||||
SET
|
||||
categorize_by = CASE
|
||||
WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)'
|
||||
WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher'
|
||||
END
|
||||
WHERE
|
||||
categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher')
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
custom_reports = frappe.get_all(
|
||||
"Report",
|
||||
filters={
|
||||
"report_type": "Custom Report",
|
||||
"reference_report": ["in", ["General Ledger", "Supplier Quotation Comparison"]],
|
||||
},
|
||||
fields=["name", "json"],
|
||||
)
|
||||
|
||||
for report in custom_reports:
|
||||
report_json = json.loads(report.json)
|
||||
|
||||
if "filters" in report_json and "group_by" in report_json["filters"]:
|
||||
report_json["filters"]["categorize_by"] = (
|
||||
report_json["filters"].pop("group_by").replace("Group", "Categorize")
|
||||
)
|
||||
|
||||
frappe.db.set_value("Report", report.name, "json", json.dumps(report_json))
|
||||
14
erpnext/patches/v14_0/set_update_price_list_based_on.py
Normal file
14
erpnext/patches/v14_0/set_update_price_list_based_on.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings",
|
||||
"update_price_list_based_on",
|
||||
(
|
||||
"Price List Rate"
|
||||
if cint(frappe.db.get_single_value("Selling Settings", "editable_price_list_rate"))
|
||||
else "Rate"
|
||||
),
|
||||
)
|
||||
15
erpnext/patches/v14_0/update_full_name_in_contract.py
Normal file
15
erpnext/patches/v14_0/update_full_name_in_contract.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
|
||||
def execute():
|
||||
con = qb.DocType("Contract")
|
||||
for c in (
|
||||
qb.from_(con)
|
||||
.select(con.name, con.party_type, con.party_name)
|
||||
.where(con.party_full_name.isnull())
|
||||
.run(as_dict=True)
|
||||
):
|
||||
field = c.party_type.lower() + "_name"
|
||||
if res := frappe.db.get_value(c.party_type, c.party_name, field):
|
||||
frappe.db.set_value("Contract", c.name, "party_full_name", res)
|
||||
@@ -174,7 +174,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
|
||||
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
|
||||
this.calculate_received_qty(doc, cdt, cdn)
|
||||
}
|
||||
super.qty(doc, cdt, cdn);
|
||||
|
||||
@@ -87,7 +87,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
|
||||
from_date: me.frm.doc.posting_date,
|
||||
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
|
||||
company: me.frm.doc.company,
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||
show_cancelled_entries: me.frm.doc.docstatus === 2,
|
||||
ignore_prepared_report: true
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
|
||||
}
|
||||
|
||||
if (item.discount_amount) {
|
||||
if (item.discount_amount > 0) {
|
||||
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
|
||||
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
|
||||
}
|
||||
@@ -336,7 +336,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
calculate_taxes() {
|
||||
var me = this;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
this.grand_total_diff = 0;
|
||||
var actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
@@ -407,7 +407,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// adjust Discount Amount loss in last tax iteration
|
||||
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
||||
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
||||
me.frm.doc.rounding_adjustment = flt(me.frm.doc.grand_total -
|
||||
me.grand_total_diff = flt(me.frm.doc.grand_total -
|
||||
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
||||
}
|
||||
}
|
||||
@@ -519,7 +519,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
adjust_grand_total_for_inclusive_tax() {
|
||||
var me = this;
|
||||
|
||||
// if fully inclusive taxes and diff
|
||||
// if any inclusive taxes and diff
|
||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
||||
var any_inclusive_tax = false;
|
||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||
@@ -530,7 +530,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
|
||||
function(d) {
|
||||
if(!d.included_in_print_rate) {
|
||||
return flt(d.tax_amount_after_discount_amount);
|
||||
let tax_amount = d.category === "Valuation" ? 0 : d.tax_amount_after_discount_amount;
|
||||
if (d.add_deduct_tax === "Deduct") tax_amount *= -1;
|
||||
return tax_amount;
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -544,9 +546,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
diff = flt(diff, precision("rounding_adjustment"));
|
||||
|
||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
||||
me.frm.doc.grand_total_diff = diff;
|
||||
} else {
|
||||
me.frm.doc.grand_total_diff = 0;
|
||||
me.grand_total_diff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,7 +557,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var me = this;
|
||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||
this.frm.doc.grand_total = flt(tax_count
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
@@ -589,9 +589,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||
- flt(this.frm.doc.rounding_adjustment), precision("total_taxes_and_charges"));
|
||||
- this.grand_total_diff, precision("total_taxes_and_charges"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges", "rounding_adjustment"]);
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||
|
||||
// Round grand total as per precision
|
||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
||||
@@ -611,6 +611,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (cint(disable_rounded_total)) {
|
||||
this.frm.doc.rounded_total = 0;
|
||||
this.frm.doc.base_rounded_total = 0;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -679,22 +680,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
return;
|
||||
}
|
||||
|
||||
var total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
var net_total = 0;
|
||||
const total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
let net_total = 0;
|
||||
let expected_net_total = 0;
|
||||
|
||||
// calculate item amount after Discount Amount
|
||||
if (total_for_discount_amount) {
|
||||
$.each(this.frm._items || [], function(i, item) {
|
||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
|
||||
|
||||
const adjusted_net_amount = item.net_amount - distributed_amount;
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, precision("net_amount", item));
|
||||
net_total += item.net_amount;
|
||||
|
||||
// discount amount rounding loss adjustment if no taxes
|
||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
||||
&& i == (me.frm._items || []).length - 1) {
|
||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
||||
- me.frm.doc.discount_amount, precision("net_total"));
|
||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
||||
precision("net_amount", item));
|
||||
// discount amount rounding adjustment
|
||||
// assignment to rounding_difference is intentional
|
||||
const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
|
||||
if (rounding_difference) {
|
||||
item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
|
||||
net_total += rounding_difference;
|
||||
}
|
||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
||||
@@ -707,29 +712,38 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
get_total_for_discount_amount() {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total") {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total")
|
||||
return this.frm.doc.net_total;
|
||||
} else {
|
||||
var total_actual_tax = 0.0;
|
||||
var actual_taxes_dict = {};
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
|
||||
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
actual_taxes_dict[tax.idx] = tax_amount;
|
||||
} else if (actual_taxes_dict[tax.row_id] !== null) {
|
||||
var actual_tax_amount = flt(actual_taxes_dict[tax.row_id]) * flt(tax.rate) / 100;
|
||||
actual_taxes_dict[tax.idx] = actual_tax_amount;
|
||||
}
|
||||
});
|
||||
let total_actual_tax = 0.0;
|
||||
let actual_taxes_dict = {};
|
||||
|
||||
$.each(actual_taxes_dict, function(key, value) {
|
||||
if (value) total_actual_tax += value;
|
||||
});
|
||||
function update_actual_taxes_dict(tax, tax_amount) {
|
||||
if (tax.add_deduct_tax == "Deduct") tax_amount *= -1;
|
||||
if (tax.category != "Valuation") total_actual_tax += tax_amount;
|
||||
|
||||
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
|
||||
actual_taxes_dict[tax.idx] = {
|
||||
tax_amount: tax_amount,
|
||||
cumulative_total: total_actual_tax
|
||||
};
|
||||
}
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
update_actual_taxes_dict(tax, tax.tax_amount);
|
||||
return;
|
||||
}
|
||||
|
||||
const base_row = actual_taxes_dict[tax.row_id];
|
||||
if (!base_row) return;
|
||||
|
||||
// if charge type is 'On Previous Row Amount', calculate tax on previous row amount
|
||||
// else (On Previous Row Total) calculate tax on cumulative total
|
||||
const base_tax_amount = tax.charge_type == "On Previous Row Amount" ? base_row["tax_amount"]: base_row["cumulative_total"];
|
||||
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
|
||||
});
|
||||
|
||||
return this.frm.doc.grand_total - total_actual_tax;
|
||||
}
|
||||
|
||||
calculate_total_advance(update_paid_amount) {
|
||||
|
||||
@@ -297,7 +297,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
|
||||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
|
||||
frm.add_custom_button(
|
||||
__("Add Sales Partners"),
|
||||
function () {
|
||||
new frappe.desk.eventParticipants(frm, "Sales Partners");
|
||||
new frappe.desk.eventParticipants(frm, "Sales Partner");
|
||||
},
|
||||
__("Add Participants")
|
||||
);
|
||||
|
||||
@@ -750,7 +750,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
});
|
||||
}
|
||||
|
||||
new frappe.ui.Dialog({
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
@@ -787,7 +787,9 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
refresh_field("items");
|
||||
},
|
||||
primary_action_label: __("Update"),
|
||||
}).show();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
};
|
||||
|
||||
erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
@@ -77,35 +77,34 @@ erpnext.accounts.dimensions = {
|
||||
},
|
||||
|
||||
update_dimension(frm, doctype) {
|
||||
if (this.accounting_dimensions) {
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
if (frm.is_new()) {
|
||||
if (
|
||||
frm.doc.company &&
|
||||
Object.keys(this.default_dimensions || {}).length > 0 &&
|
||||
this.default_dimensions[frm.doc.company]
|
||||
) {
|
||||
let default_dimension =
|
||||
this.default_dimensions[frm.doc.company][dimension["fieldname"]];
|
||||
if (
|
||||
!this.accounting_dimensions ||
|
||||
!frm.is_new() ||
|
||||
!frm.doc.company ||
|
||||
!this.default_dimensions?.[frm.doc.company]
|
||||
)
|
||||
return;
|
||||
|
||||
if (default_dimension) {
|
||||
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
|
||||
frm.set_value(dimension["fieldname"], default_dimension);
|
||||
}
|
||||
|
||||
$.each(frm.doc.items || frm.doc.accounts || [], function (i, row) {
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
row.name,
|
||||
dimension["fieldname"],
|
||||
default_dimension
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// don't set default dimensions if any of the dimension is already set due to mapping
|
||||
if (frm.doc.__onload?.load_after_mapping) {
|
||||
for (const dimension of this.accounting_dimensions) {
|
||||
if (frm.doc[dimension["fieldname"]]) return;
|
||||
}
|
||||
}
|
||||
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]];
|
||||
|
||||
if (!default_dimension) return;
|
||||
|
||||
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
|
||||
frm.set_value(dimension["fieldname"], default_dimension);
|
||||
}
|
||||
|
||||
(frm.doc.items || frm.doc.accounts || []).forEach((row) => {
|
||||
frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {
|
||||
|
||||
@@ -121,10 +121,10 @@ erpnext.accounts.unreconcile_payment = {
|
||||
};
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "UnReconcile Allocations",
|
||||
title: __("UnReconcile Allocations"),
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: "large",
|
||||
primary_action_label: "UnReconcile",
|
||||
primary_action_label: __("UnReconcile"),
|
||||
primary_action(values) {
|
||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||
if (selected_allocations.length > 0) {
|
||||
@@ -138,7 +138,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
);
|
||||
d.hide();
|
||||
} else {
|
||||
frappe.msgprint("No Selection");
|
||||
frappe.msgprint(__("No Selection"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
|
||||
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||
|
||||
@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
|
||||
|
||||
frm.add_fetch("lead_name", "company_name", "customer_name");
|
||||
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
|
||||
frm.set_query("customer_group", { is_group: 0 });
|
||||
frm.set_query("default_price_list", { selling: 1 });
|
||||
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
@@ -1266,6 +1266,11 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
|
||||
items.update({"company": company, "sales_order": sales_order})
|
||||
|
||||
item_wh = {}
|
||||
for item in items.get("items"):
|
||||
if item.get("warehouse"):
|
||||
item_wh[item.get("item_code")] = item.get("warehouse")
|
||||
|
||||
raw_materials = get_items_for_material_requests(items)
|
||||
if not raw_materials:
|
||||
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
||||
@@ -1290,7 +1295,7 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
"item_code": item.get("item_code"),
|
||||
"qty": item.get("quantity"),
|
||||
"schedule_date": schedule_date,
|
||||
"warehouse": item.get("warehouse"),
|
||||
"warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"),
|
||||
"sales_order": sales_order,
|
||||
"project": project,
|
||||
},
|
||||
|
||||
@@ -853,7 +853,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
def test_auto_insert_price(self):
|
||||
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
||||
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings",
|
||||
{
|
||||
"auto_insert_price_list_rate_if_missing": 1,
|
||||
"update_price_list_based_on": "Price List Rate",
|
||||
},
|
||||
)
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}
|
||||
@@ -865,6 +871,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100
|
||||
)
|
||||
|
||||
# ensure price gets inserted based on rate if price list rate is not defined by user
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Price",
|
||||
@@ -874,6 +881,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
100,
|
||||
)
|
||||
|
||||
# ensure price gets insterted based on user-defined *Price List Rate*
|
||||
# if update_price_list_based_on is set to Price List Rate
|
||||
make_sales_order(
|
||||
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||
selling_price_list="_Test Price List",
|
||||
@@ -881,18 +890,43 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
discount_percentage=20,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Price",
|
||||
{
|
||||
"price_list": "_Test Price List",
|
||||
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||
},
|
||||
"price_list_rate",
|
||||
),
|
||||
200,
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price",
|
||||
{
|
||||
"price_list": "_Test Price List",
|
||||
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||
},
|
||||
("name", "price_list_rate"),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(item_price.price_list_rate, 200)
|
||||
frappe.delete_doc("Item Price", item_price.name)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "update_price_list_based_on", "Rate")
|
||||
|
||||
# ensure price gets insterted based on user-defined *Rate*
|
||||
# if update_price_list_based_on is set to Rate
|
||||
make_sales_order(
|
||||
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||
selling_price_list="_Test Price List",
|
||||
price_list_rate=200,
|
||||
discount_percentage=20,
|
||||
)
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price",
|
||||
{
|
||||
"price_list": "_Test Price List",
|
||||
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||
},
|
||||
("name", "price_list_rate"),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(item_price.price_list_rate, 160)
|
||||
frappe.delete_doc("Item Price", item_price.name)
|
||||
|
||||
# do not update price list
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
@@ -917,6 +951,63 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||
|
||||
def test_update_existing_item_price(self):
|
||||
item_code = "_Test Item for Price List Updation"
|
||||
price_list = "_Test Price List"
|
||||
|
||||
make_item(item_code, {"is_stock_item": 0})
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings",
|
||||
{
|
||||
"auto_insert_price_list_rate_if_missing": 1,
|
||||
"update_existing_price_list_rate": 1,
|
||||
"update_price_list_based_on": "Rate",
|
||||
},
|
||||
)
|
||||
|
||||
# setup: price creation
|
||||
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=100)
|
||||
|
||||
# test price updation based on Rate
|
||||
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=90)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Price",
|
||||
{"price_list": price_list, "item_code": item_code},
|
||||
"price_list_rate",
|
||||
),
|
||||
90,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings",
|
||||
{
|
||||
"update_price_list_based_on": "Price List Rate",
|
||||
},
|
||||
)
|
||||
|
||||
# test price updation based on Price List Rate
|
||||
make_sales_order(
|
||||
item_code=item_code,
|
||||
selling_price_list=price_list,
|
||||
price_list_rate=200,
|
||||
discount_percentage=20,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Price",
|
||||
{"price_list": price_list, "item_code": item_code},
|
||||
"price_list_rate",
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
# reset `update_existing_price_list_rate` to 0
|
||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||
|
||||
def test_drop_shipping(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Selling Settings", {
|
||||
refresh: function (frm) {},
|
||||
after_save(frm) {
|
||||
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_all(
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
)
|
||||
invoices_by_name = frappe.db.get_all(
|
||||
invoices_by_name = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
@@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
|
||||
invoice_list = invoices_by_customer + invoices_by_name
|
||||
elif status:
|
||||
invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields)
|
||||
invoice_list = frappe.db.get_list("POS Invoice", filters={"status": status}, fields=fields)
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ class Company(NestedSet):
|
||||
frappe.clear_cache()
|
||||
|
||||
def create_default_warehouses(self):
|
||||
parent_warehouse = None
|
||||
for wh_detail in [
|
||||
{"warehouse_name": _("All Warehouses"), "is_group": 1},
|
||||
{"warehouse_name": _("Stores"), "is_group": 0},
|
||||
@@ -174,24 +175,31 @@ class Company(NestedSet):
|
||||
{"warehouse_name": _("Finished Goods"), "is_group": 0},
|
||||
{"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"},
|
||||
]:
|
||||
if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)):
|
||||
warehouse = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"is_group": wh_detail["is_group"],
|
||||
"company": self.name,
|
||||
"parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr)
|
||||
if not wh_detail["is_group"]
|
||||
else "",
|
||||
"warehouse_type": wh_detail["warehouse_type"]
|
||||
if "warehouse_type" in wh_detail
|
||||
else None,
|
||||
}
|
||||
)
|
||||
warehouse.flags.ignore_permissions = True
|
||||
warehouse.flags.ignore_mandatory = True
|
||||
warehouse.insert()
|
||||
if frappe.db.exists(
|
||||
"Warehouse",
|
||||
{
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"company": self.name,
|
||||
},
|
||||
):
|
||||
continue
|
||||
|
||||
warehouse = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"is_group": wh_detail["is_group"],
|
||||
"company": self.name,
|
||||
"parent_warehouse": parent_warehouse,
|
||||
"warehouse_type": wh_detail.get("warehouse_type"),
|
||||
}
|
||||
)
|
||||
warehouse.flags.ignore_permissions = True
|
||||
warehouse.flags.ignore_mandatory = True
|
||||
warehouse.insert()
|
||||
|
||||
if wh_detail["is_group"]:
|
||||
parent_warehouse = warehouse.name
|
||||
|
||||
def create_default_accounts(self):
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
|
||||
|
||||
@@ -2,8 +2,10 @@ frappe.listview_settings["Employee"] = {
|
||||
add_fields: ["status", "branch", "department", "designation", "image"],
|
||||
filters: [["status", "=", "Active"]],
|
||||
get_indicator: function (doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status];
|
||||
return indicator;
|
||||
return [
|
||||
__(doc.status, null, "Employee"),
|
||||
{ Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status],
|
||||
"status,=," + doc.status,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -190,35 +190,42 @@ class TransactionDeletionRecord(Document):
|
||||
"""Delete addresses to which leads are linked"""
|
||||
self.validate_doc_status()
|
||||
if not self.delete_leads_and_addresses:
|
||||
leads = frappe.get_all("Lead", filters={"company": self.company})
|
||||
leads = ["'%s'" % row.get("name") for row in leads]
|
||||
leads = frappe.db.get_all("Lead", filters={"company": self.company}, pluck="name")
|
||||
addresses = []
|
||||
if leads:
|
||||
addresses = frappe.db.sql_list(
|
||||
"""select parent from `tabDynamic Link` where link_name
|
||||
in ({leads})""".format(leads=",".join(leads))
|
||||
addresses = frappe.db.get_all(
|
||||
"Dynamic Link", filters={"link_name": ("in", leads)}, pluck="parent"
|
||||
)
|
||||
|
||||
if addresses:
|
||||
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tabAddress` where name in ({addresses}) and
|
||||
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
|
||||
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
|
||||
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))
|
||||
)
|
||||
address = qb.DocType("Address")
|
||||
dl1 = qb.DocType("Dynamic Link")
|
||||
dl2 = qb.DocType("Dynamic Link")
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tabDynamic Link` where link_doctype='Lead'
|
||||
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))
|
||||
)
|
||||
qb.from_(address).delete().where(
|
||||
(address.name.isin(addresses))
|
||||
& (
|
||||
address.name.notin(
|
||||
qb.from_(dl1)
|
||||
.join(dl2)
|
||||
.on((dl1.parent == dl2.parent) & (dl1.link_doctype != dl2.link_doctype))
|
||||
.select(dl1.parent)
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
dynamic_link = qb.DocType("Dynamic Link")
|
||||
qb.from_(dynamic_link).delete().where(
|
||||
(dynamic_link.link_doctype == "Lead")
|
||||
& (dynamic_link.parenttype == "Address")
|
||||
& (dynamic_link.link_name.isin(leads))
|
||||
).run()
|
||||
|
||||
customer = qb.DocType("Customer")
|
||||
qb.update(customer).set(customer.lead_name, None).where(customer.lead_name.isin(leads)).run()
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
|
||||
leads=",".join(leads)
|
||||
)
|
||||
)
|
||||
self.db_set("delete_leads_and_addresses", 1)
|
||||
self.enqueue_task(task="Reset Company Values")
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user