mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 12:39:18 +00:00
Compare commits
345 Commits
revert-347
...
v13.52.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9876019c69 | ||
|
|
f4fb878282 | ||
|
|
783bb93913 | ||
|
|
cfbd9af100 | ||
|
|
966c296872 | ||
|
|
0ff871e38e | ||
|
|
9df10dbc40 | ||
|
|
bdaae81171 | ||
|
|
066cf0e3bc | ||
|
|
829298066f | ||
|
|
006da22d3f | ||
|
|
3b9ac9f46a | ||
|
|
8f977f40f0 | ||
|
|
3bc7c88133 | ||
|
|
84b6f68108 | ||
|
|
e5b38607ce | ||
|
|
b3c9d0d910 | ||
|
|
c417365e03 | ||
|
|
b2a4175d43 | ||
|
|
a801bba83e | ||
|
|
3ccb511e25 | ||
|
|
8c51f2e5a1 | ||
|
|
5dbca09899 | ||
|
|
9ec7bb9be3 | ||
|
|
0281afcead | ||
|
|
348e4616cb | ||
|
|
a165b37fd7 | ||
|
|
cdc86bd76c | ||
|
|
e9df06406f | ||
|
|
c574494ddd | ||
|
|
46966f4b7c | ||
|
|
ded3b62c5a | ||
|
|
a5e1c4798f | ||
|
|
bde9e89582 | ||
|
|
e4f28e8a5b | ||
|
|
128ea0d7fc | ||
|
|
9566f4101d | ||
|
|
4854c2e7f7 | ||
|
|
a93c4b6c65 | ||
|
|
287c67f7a2 | ||
|
|
120de249dd | ||
|
|
cefa78b864 | ||
|
|
73366de12f | ||
|
|
1d3917b335 | ||
|
|
3bc899f354 | ||
|
|
6b113c6abc | ||
|
|
82e2ff8731 | ||
|
|
2993eb5ce9 | ||
|
|
12e27e96a8 | ||
|
|
30c9b15cdd | ||
|
|
1d6bc68d87 | ||
|
|
ba69be1ced | ||
|
|
4af57a7318 | ||
|
|
e05bb103f3 | ||
|
|
6f2fb89c49 | ||
|
|
beaf13e00e | ||
|
|
9c3ec41803 | ||
|
|
06c85fa252 | ||
|
|
0248fee533 | ||
|
|
5aa02b8571 | ||
|
|
53f7764c67 | ||
|
|
cf3ec935a7 | ||
|
|
2732276498 | ||
|
|
77eb11a6e3 | ||
|
|
5cfe4195ba | ||
|
|
0feb393fff | ||
|
|
4b20f2a083 | ||
|
|
b992366246 | ||
|
|
6490b7d561 | ||
|
|
a4d8f9cb94 | ||
|
|
2893ae72f5 | ||
|
|
93acde7748 | ||
|
|
3a00052b49 | ||
|
|
1116cee831 | ||
|
|
a7d26b0c20 | ||
|
|
a370dc3dcc | ||
|
|
b04c190e33 | ||
|
|
77d019cc3b | ||
|
|
fd84119273 | ||
|
|
9b9d839835 | ||
|
|
0ef0ff470f | ||
|
|
10c9640cbd | ||
|
|
da1218f324 | ||
|
|
407e5b5fa3 | ||
|
|
b9833db7bd | ||
|
|
d4e680c109 | ||
|
|
b63fbe4286 | ||
|
|
0602ddcfc8 | ||
|
|
18c3a668d9 | ||
|
|
26489121f3 | ||
|
|
41902c3676 | ||
|
|
0bcd0476a2 | ||
|
|
41344593c9 | ||
|
|
bcfd7708f2 | ||
|
|
4dd088cba4 | ||
|
|
12b62571b8 | ||
|
|
3785fe6927 | ||
|
|
de529f0adf | ||
|
|
2e2c319f20 | ||
|
|
8939e95c62 | ||
|
|
6c4dff38da | ||
|
|
000ebe4479 | ||
|
|
0fe95bf77e | ||
|
|
60a170d1a4 | ||
|
|
51dd0ec876 | ||
|
|
aa8446d794 | ||
|
|
7239e839a0 | ||
|
|
04990d51db | ||
|
|
0ec74b059e | ||
|
|
2a6e80214c | ||
|
|
af10d8080b | ||
|
|
e899c30428 | ||
|
|
a53832e16e | ||
|
|
c6885e6789 | ||
|
|
f22969d266 | ||
|
|
75b3423ab1 | ||
|
|
3d0add81fa | ||
|
|
46d0b7d317 | ||
|
|
0a8b7148a5 | ||
|
|
6400a574b6 | ||
|
|
eaa1589331 | ||
|
|
d49a8ad74f | ||
|
|
a98a13b683 | ||
|
|
e37b6bbbf1 | ||
|
|
bcc8a45c4e | ||
|
|
571c977e8e | ||
|
|
ac9f1fefe6 | ||
|
|
513da54b6d | ||
|
|
f8a8cf3046 | ||
|
|
1685305b53 | ||
|
|
b95d459812 | ||
|
|
0e11317303 | ||
|
|
9a659254e3 | ||
|
|
e75ca14a88 | ||
|
|
c2bf8e3502 | ||
|
|
4d4f218175 | ||
|
|
4c2c037a86 | ||
|
|
4f79214ae6 | ||
|
|
5b37abd2d6 | ||
|
|
a24d488817 | ||
|
|
986a90efe0 | ||
|
|
4a35ff0e57 | ||
|
|
8e3636ff53 | ||
|
|
1b69b37229 | ||
|
|
97f4af8d97 | ||
|
|
9d5b500060 | ||
|
|
3831c7920d | ||
|
|
f182fc1f8e | ||
|
|
1897d6f214 | ||
|
|
1415f40dfb | ||
|
|
09cf050b0d | ||
|
|
55448017d7 | ||
|
|
202513ae6a | ||
|
|
e3a8a8d195 | ||
|
|
169af8f9f8 | ||
|
|
f0580b0e4d | ||
|
|
b5b34c14b2 | ||
|
|
839a1f0454 | ||
|
|
63fba9db39 | ||
|
|
00fd08c7bc | ||
|
|
d8dd22adaf | ||
|
|
6f43829c32 | ||
|
|
3e95d56240 | ||
|
|
44cb62824d | ||
|
|
022893391b | ||
|
|
139a193f1d | ||
|
|
4f5ee6876d | ||
|
|
270eb1db4d | ||
|
|
20d3381010 | ||
|
|
fd04bd0f72 | ||
|
|
166ec0e58c | ||
|
|
1e1dddfe6c | ||
|
|
0a42e6ff0f | ||
|
|
97f9c0d53f | ||
|
|
137898d55d | ||
|
|
f65be40037 | ||
|
|
75f4a616f1 | ||
|
|
8d97f8b0b7 | ||
|
|
f63b866de3 | ||
|
|
d6427cfe53 | ||
|
|
774092343a | ||
|
|
2aa7729243 | ||
|
|
66ba74f3fc | ||
|
|
dc04b24234 | ||
|
|
eb243c2470 | ||
|
|
f7ed4ecd56 | ||
|
|
6bc8749eaf | ||
|
|
2747df78ac | ||
|
|
d316955d18 | ||
|
|
2e3f8e8846 | ||
|
|
9af4e117d4 | ||
|
|
6191cfee4c | ||
|
|
f0c9d89aab | ||
|
|
06deecbd92 | ||
|
|
6c170abdf9 | ||
|
|
7506132861 | ||
|
|
387f8b9e1a | ||
|
|
c2ae8eaec0 | ||
|
|
f5f88bb62c | ||
|
|
188cfc2e3c | ||
|
|
c7c2bad6ab | ||
|
|
e6a9252f79 | ||
|
|
4a9ad09c7f | ||
|
|
e37b9030fb | ||
|
|
77f548c814 | ||
|
|
7626d51db1 | ||
|
|
48e5846ed5 | ||
|
|
ac26e4ba2a | ||
|
|
8b9f8c6ab7 | ||
|
|
a1d717053a | ||
|
|
8b3d6ee7b0 | ||
|
|
1380f7a7ec | ||
|
|
2825253339 | ||
|
|
40cfd5215c | ||
|
|
af8142cf85 | ||
|
|
e2af66c7be | ||
|
|
ef2d4febdd | ||
|
|
cb0d567d7b | ||
|
|
9a376039aa | ||
|
|
2f74026513 | ||
|
|
740313ff09 | ||
|
|
db6d0e03f5 | ||
|
|
778ba6956c | ||
|
|
b19b0a4a98 | ||
|
|
b31d8eec05 | ||
|
|
078161cf6b | ||
|
|
635559d905 | ||
|
|
6bdf143084 | ||
|
|
198a64d574 | ||
|
|
7d6e2f979f | ||
|
|
6992e727cf | ||
|
|
a852dc1f11 | ||
|
|
c5261cde9c | ||
|
|
d3c769c183 | ||
|
|
563e5c0b69 | ||
|
|
5746ddce84 | ||
|
|
54388e8d92 | ||
|
|
784ea7cf48 | ||
|
|
fc42e026ab | ||
|
|
cef7126a35 | ||
|
|
297facc1cb | ||
|
|
31bda37970 | ||
|
|
1d6917f340 | ||
|
|
b85d8946f7 | ||
|
|
5f28b1d330 | ||
|
|
61a3121172 | ||
|
|
d5a80b5615 | ||
|
|
189b020d22 | ||
|
|
be2095ad03 | ||
|
|
83afaf48df | ||
|
|
9087ac0829 | ||
|
|
7759d1e390 | ||
|
|
cfa1a2b050 | ||
|
|
c1187bed26 | ||
|
|
9957981039 | ||
|
|
82b46f2bfe | ||
|
|
91b5a33564 | ||
|
|
d215a85747 | ||
|
|
813b4d4de2 | ||
|
|
983140acd8 | ||
|
|
102ac9f74d | ||
|
|
ebf8deb933 | ||
|
|
5680045f2b | ||
|
|
d010b048dc | ||
|
|
046bf64fa3 | ||
|
|
b73422e4ee | ||
|
|
acecd07fa2 | ||
|
|
5c6134f1b0 | ||
|
|
a2d2beb610 | ||
|
|
3f4c322bef | ||
|
|
ba984acef2 | ||
|
|
98ed6445a8 | ||
|
|
5d511035ec | ||
|
|
7655a4f0d1 | ||
|
|
c93a5ab8f0 | ||
|
|
12cbe38299 | ||
|
|
77f1322732 | ||
|
|
8609bf4a12 | ||
|
|
71bafab41b | ||
|
|
ce151ddae4 | ||
|
|
16ae117c97 | ||
|
|
21cd789842 | ||
|
|
98de1f201d | ||
|
|
034e35e7f6 | ||
|
|
2c40be2337 | ||
|
|
278f38f2aa | ||
|
|
b1bb749e23 | ||
|
|
4cf66f0585 | ||
|
|
0da6237d22 | ||
|
|
526e350d98 | ||
|
|
8c4f45307e | ||
|
|
7b2dc2449d | ||
|
|
78bd698f9e | ||
|
|
544e37ca5c | ||
|
|
c7bdb1bbf9 | ||
|
|
5f5fa843ac | ||
|
|
f17b2de420 | ||
|
|
d5efeec0a4 | ||
|
|
6e492ec514 | ||
|
|
cfcbdfcaec | ||
|
|
499987040b | ||
|
|
5157f5dd0e | ||
|
|
9ce5d84951 | ||
|
|
b712aea3a4 | ||
|
|
07ff956fd8 | ||
|
|
c575942acf | ||
|
|
6841e22ffe | ||
|
|
56a422deed | ||
|
|
0ec34e5880 | ||
|
|
ba58c7ed59 | ||
|
|
178be42369 | ||
|
|
b4e775b264 | ||
|
|
e6945508f1 | ||
|
|
573cd3c33b | ||
|
|
b6edadb3cb | ||
|
|
c4d9576f9f | ||
|
|
74303b65cf | ||
|
|
a34aff6f49 | ||
|
|
5f25cea322 | ||
|
|
6a0c24e7b3 | ||
|
|
8eb6053c97 | ||
|
|
c8ec365594 | ||
|
|
0490e3bfe6 | ||
|
|
9766827a08 | ||
|
|
eeaa8b2479 | ||
|
|
4a95c9d642 | ||
|
|
f6707b2b92 | ||
|
|
62dc68bb57 | ||
|
|
f80fb97c71 | ||
|
|
6308fca587 | ||
|
|
ab71a7bba8 | ||
|
|
958a3320e8 | ||
|
|
6a9660de65 | ||
|
|
edbbb2469f | ||
|
|
e3ad0b1655 | ||
|
|
81e4be37ff | ||
|
|
62edb118eb | ||
|
|
71395b9a8e | ||
|
|
d3aa37aece | ||
|
|
550daf2108 | ||
|
|
959eae1b5c | ||
|
|
ab30e2a9c7 | ||
|
|
65dd72a0b0 | ||
|
|
3efa5215a0 | ||
|
|
1fa0fe7434 |
38
.github/workflows/release_notes.yml
vendored
Normal file
38
.github/workflows/release_notes.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# This action:
|
||||
#
|
||||
# 1. Generates release notes using github API.
|
||||
# 2. Strips unnecessary info like chore/style etc from notes.
|
||||
# 3. Updates release info.
|
||||
|
||||
# This action needs to be maintained on all branches that do releases.
|
||||
|
||||
name: 'Release Notes'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Tag of release like v13.0.0'
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
regen-notes:
|
||||
name: 'Regenerate release notes'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Update notes
|
||||
run: |
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
|
||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
|
||||
29
CODEOWNERS
29
CODEOWNERS
@@ -3,14 +3,13 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/erpnext_integrations/ @nextchamp-saqib
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
@@ -18,16 +17,10 @@ erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @rohitwaghchaure
|
||||
requirements.txt @ankush
|
||||
|
||||
erpnext/healthcare/ @chillaranand
|
||||
erpnext/hr/ @ruchamahabal
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
erpnext/payroll @ruchamahabal
|
||||
erpnext/projects/ @ruchamahabal
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @ankush
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/public/ @nextchamp-saqib @marination
|
||||
|
||||
.github/ @ankush
|
||||
requirements.txt @gavindsouza @ankush
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.42.7"
|
||||
__version__ = "13.52.12"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -297,7 +297,7 @@ def _make_test_records(verbose=None):
|
||||
# fixed asset depreciation
|
||||
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
|
||||
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
|
||||
["_Test Depreciations", "Expenses", 0, None, None],
|
||||
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
|
||||
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
# Receivable / Payable Account
|
||||
["_Test Receivable", "Current Assets", 0, "Receivable", None],
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"submit_journal_entries",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
@@ -175,6 +176,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
@@ -292,6 +294,12 @@
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -299,7 +307,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-28 09:50:20.375233",
|
||||
"modified": "2023-06-13 18:47:46.430291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -56,7 +56,7 @@ class BankClearance(Document):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Asset', 'Asset Movement'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -75,6 +75,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
@@ -134,6 +135,13 @@ class JournalEntry(AccountsController):
|
||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||
|
||||
def validate_depr_entry_voucher_type(self):
|
||||
if (
|
||||
any(d.account_type == "Depreciation" for d in self.get("accounts"))
|
||||
and self.voucher_type != "Depreciation Entry"
|
||||
):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
for account in stock_accounts:
|
||||
@@ -237,25 +245,30 @@ class JournalEntry(AccountsController):
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
continue
|
||||
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation -= d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
|
||||
|
||||
asset.set_status()
|
||||
|
||||
@@ -317,38 +330,45 @@ class JournalEntry(AccountsController):
|
||||
d.db_update()
|
||||
|
||||
def unlink_asset_reference(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
fb_idx = None
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.set_status()
|
||||
|
||||
fb_idx = cint(s.finance_book_id) or 1
|
||||
break
|
||||
if not fb_idx:
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation += d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
depr_value = d.debit or d.credit
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
|
||||
journal_entry_for_scrap = frappe.db.get_value(
|
||||
"Asset", d.reference_name, "journal_entry_for_scrap"
|
||||
)
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
|
||||
|
||||
asset.set_status()
|
||||
if journal_entry_for_scrap == self.name:
|
||||
frappe.throw(
|
||||
_("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.")
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -380,6 +400,15 @@ class JournalEntry(AccountsController):
|
||||
d.idx, d.account
|
||||
)
|
||||
)
|
||||
elif (
|
||||
d.party_type
|
||||
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
|
||||
d.idx, d.account, d.party_type
|
||||
)
|
||||
)
|
||||
|
||||
def check_credit_limit(self):
|
||||
customers = list(
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
onload: function(frm) {
|
||||
if(frm.is_new()) {
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message) {
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frappe.model.set_default_values(frm.doc);
|
||||
|
||||
@@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message){
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
voucher_type: function(frm) {
|
||||
var add_accounts = function(doc, r) {
|
||||
|
||||
@@ -623,7 +623,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
get_outstanding_invoice: function(frm) {
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
@@ -653,12 +653,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
btn_text = "Get Outstanding Invoices";
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
btn_text = "Get Outstanding Orders";
|
||||
}
|
||||
|
||||
frappe.prompt(fields, function(filters){
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
frm.events.validate_filters_data(frm, filters);
|
||||
frm.doc.cost_center = filters.cost_center;
|
||||
frm.events.get_outstanding_documents(frm, filters);
|
||||
}, __("Filters"), __("Get Outstanding Documents"));
|
||||
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
|
||||
}, __("Filters"), __(btn_text));
|
||||
},
|
||||
|
||||
get_outstanding_invoices: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, true, false);
|
||||
},
|
||||
|
||||
get_outstanding_orders: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, false, true);
|
||||
},
|
||||
|
||||
validate_filters_data: function(frm, filters) {
|
||||
@@ -684,7 +701,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
},
|
||||
|
||||
get_outstanding_documents: function(frm, filters) {
|
||||
get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
frm.clear_table("references");
|
||||
|
||||
if(!frm.doc.party) {
|
||||
@@ -708,6 +725,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
args[key] = filters[key];
|
||||
}
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
args["get_outstanding_invoices"] = true;
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
args["get_orders_to_be_billed"] = true;
|
||||
}
|
||||
|
||||
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
||||
|
||||
return frappe.call({
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"base_received_amount",
|
||||
"base_received_amount_after_tax",
|
||||
"section_break_14",
|
||||
"get_outstanding_invoice",
|
||||
"get_outstanding_invoices",
|
||||
"get_outstanding_orders",
|
||||
"references",
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
@@ -353,12 +354,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoice",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "references",
|
||||
"fieldtype": "Table",
|
||||
@@ -726,12 +721,24 @@
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoices",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoices"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_orders",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Orders"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-23 20:08:39.559814",
|
||||
"modified": "2023-06-19 11:38:04.387219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -7,7 +7,16 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, get_link_to_form, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
comma_and,
|
||||
comma_or,
|
||||
flt,
|
||||
fmt_money,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
from six import iteritems, string_types
|
||||
|
||||
import erpnext
|
||||
@@ -150,19 +159,68 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def validate_allocated_amount(self):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0:
|
||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0:
|
||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -270,7 +328,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_party_details(self):
|
||||
if self.party:
|
||||
if not frappe.db.exists(self.party_type, self.party):
|
||||
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
||||
|
||||
def set_exchange_rate(self, ref_doc=None):
|
||||
self.set_source_exchange_rate(ref_doc)
|
||||
@@ -327,7 +385,9 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes))
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or((_(d) for d in valid_reference_doctypes))
|
||||
)
|
||||
)
|
||||
|
||||
elif d.reference_name:
|
||||
@@ -340,7 +400,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.party != ref_doc.get(scrub(self.party_type)):
|
||||
frappe.throw(
|
||||
_("{0} {1} is not associated with {2} {3}").format(
|
||||
d.reference_doctype, d.reference_name, self.party_type, self.party
|
||||
_(d.reference_doctype), d.reference_name, _(self.party_type), self.party
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -361,18 +421,18 @@ class PaymentEntry(AccountsController):
|
||||
if ref_party_account != self.party_account:
|
||||
frappe.throw(
|
||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||
d.reference_doctype, d.reference_name, ref_party_account, self.party_account
|
||||
_(d.reference_doctype), d.reference_name, ref_party_account, self.party_account
|
||||
)
|
||||
)
|
||||
|
||||
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||
frappe.throw(
|
||||
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
|
||||
title=_("Invalid Invoice"),
|
||||
_("{0} {1} is on hold").format(_(d.reference_doctype), d.reference_name),
|
||||
title=_("Invalid Purchase Invoice"),
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
||||
frappe.throw(_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name))
|
||||
|
||||
def validate_paid_invoices(self):
|
||||
no_oustanding_refs = {}
|
||||
@@ -388,14 +448,13 @@ class PaymentEntry(AccountsController):
|
||||
if outstanding_amount <= 0 and not is_return:
|
||||
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
|
||||
|
||||
for k, v in no_oustanding_refs.items():
|
||||
for reference_doctype, references in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
|
||||
"References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount."
|
||||
).format(
|
||||
_(k),
|
||||
frappe.bold(", ".join(d.reference_name for d in v)),
|
||||
frappe.bold(_("negative outstanding amount")),
|
||||
frappe.bold(comma_and((d.reference_name for d in references))),
|
||||
_(reference_doctype),
|
||||
)
|
||||
+ "<br><br>"
|
||||
+ _("If this is undesirable please cancel the corresponding Payment Entry."),
|
||||
@@ -430,7 +489,7 @@ class PaymentEntry(AccountsController):
|
||||
if not valid:
|
||||
frappe.throw(
|
||||
_("Against Journal Entry {0} does not have any unmatched {1} entry").format(
|
||||
d.reference_name, dr_or_cr
|
||||
d.reference_name, _(dr_or_cr)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -497,7 +556,7 @@ class PaymentEntry(AccountsController):
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot allocate more than {1} against payment term {2}").format(
|
||||
idx, outstanding, key[0]
|
||||
idx, fmt_money(outstanding), key[0]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -557,7 +616,9 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
net_total = self.paid_amount
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -602,6 +663,20 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_net_total)"]
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -774,7 +849,7 @@ class PaymentEntry(AccountsController):
|
||||
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
|
||||
_(self.payment_type),
|
||||
(_("to") if self.party_type == "Customer" else _("from")),
|
||||
self.party_type,
|
||||
_(self.party_type),
|
||||
),
|
||||
InvalidPaymentEntry,
|
||||
)
|
||||
@@ -782,7 +857,7 @@ class PaymentEntry(AccountsController):
|
||||
elif paid_amount - additional_charges > total_negative_outstanding:
|
||||
frappe.throw(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
fmt_money(total_negative_outstanding)
|
||||
),
|
||||
InvalidPaymentEntry,
|
||||
)
|
||||
@@ -1278,6 +1353,9 @@ def get_outstanding_reference_documents(args):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
|
||||
args["get_outstanding_invoices"] = True
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
if args.get("party_type") == "Supplier":
|
||||
supplier_status = get_supplier_block_status(args["party"])
|
||||
@@ -1318,32 +1396,48 @@ def get_outstanding_reference_documents(args):
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
args.get("company"),
|
||||
filters=args,
|
||||
condition=condition,
|
||||
)
|
||||
outstanding_invoices = []
|
||||
negative_outstanding_invoices = []
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
if args.get("get_outstanding_invoices"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
args.get("company"),
|
||||
filters=args,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
for d in outstanding_invoices:
|
||||
d["exchange_rate"] = 1
|
||||
if party_account_currency != company_currency:
|
||||
if d.voucher_type in ("Sales Invoice", "Purchase Invoice", "Expense Claim"):
|
||||
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
d["exchange_rate"] = get_exchange_rate(
|
||||
party_account_currency, company_currency, d.posting_date
|
||||
)
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
|
||||
for d in outstanding_invoices:
|
||||
d["exchange_rate"] = 1
|
||||
if party_account_currency != company_currency:
|
||||
if d.voucher_type in ("Sales Invoice", "Purchase Invoice", "Expense Claim"):
|
||||
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
d["exchange_rate"] = get_exchange_rate(
|
||||
party_account_currency, company_currency, d.posting_date
|
||||
)
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
party_account_currency,
|
||||
company_currency,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||
orders_to_be_billed = []
|
||||
if args.get("party_type") != "Student":
|
||||
if args.get("get_orders_to_be_billed") and args.get("party_type") != "Student":
|
||||
orders_to_be_billed = get_orders_to_be_billed(
|
||||
args.get("posting_date"),
|
||||
args.get("party_type"),
|
||||
@@ -1354,25 +1448,22 @@ def get_outstanding_reference_documents(args):
|
||||
filters=args,
|
||||
)
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
party_account_currency,
|
||||
company_currency,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
if not data:
|
||||
if args.get("get_outstanding_invoices") and args.get("get_orders_to_be_billed"):
|
||||
ref_document_type = "invoices or orders"
|
||||
elif args.get("get_outstanding_invoices"):
|
||||
ref_document_type = "invoices"
|
||||
elif args.get("get_orders_to_be_billed"):
|
||||
ref_document_type = "orders"
|
||||
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
|
||||
).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
|
||||
"No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||
).format(
|
||||
_(ref_document_type), _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
@@ -1446,66 +1537,71 @@ def get_orders_to_be_billed(
|
||||
cost_center=None,
|
||||
filters=None,
|
||||
):
|
||||
voucher_type = None
|
||||
if party_type == "Customer":
|
||||
voucher_type = "Sales Order"
|
||||
elif party_type == "Supplier":
|
||||
voucher_type = "Purchase Order"
|
||||
elif party_type == "Employee":
|
||||
voucher_type = None
|
||||
|
||||
if not voucher_type:
|
||||
return []
|
||||
|
||||
# Add cost center condition
|
||||
if voucher_type:
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center"):
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
orders = []
|
||||
if voucher_type:
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
rounded_total_field = "base_rounded_total"
|
||||
else:
|
||||
grand_total_field = "grand_total"
|
||||
rounded_total_field = "rounded_total"
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
rounded_total_field = "base_rounded_total"
|
||||
else:
|
||||
grand_total_field = "grand_total"
|
||||
rounded_total_field = "rounded_total"
|
||||
|
||||
orders = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as voucher_no,
|
||||
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
|
||||
(if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
|
||||
transaction_date as posting_date
|
||||
from
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s
|
||||
and docstatus = 1
|
||||
and company = %s
|
||||
and ifnull(status, "") != "Closed"
|
||||
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
|
||||
and abs(100 - per_billed) > 0.01
|
||||
{condition}
|
||||
order by
|
||||
transaction_date, name
|
||||
""".format(
|
||||
**{
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"condition": condition,
|
||||
}
|
||||
),
|
||||
(party, company),
|
||||
as_dict=True,
|
||||
)
|
||||
orders = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as voucher_no,
|
||||
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
|
||||
(if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
|
||||
transaction_date as posting_date
|
||||
from
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s
|
||||
and docstatus = 1
|
||||
and company = %s
|
||||
and ifnull(status, "") != "Closed"
|
||||
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
|
||||
and abs(100 - per_billed) > 0.01
|
||||
{condition}
|
||||
order by
|
||||
transaction_date, name
|
||||
""".format(
|
||||
**{
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"condition": condition,
|
||||
}
|
||||
),
|
||||
(party, company),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
order_list = []
|
||||
for d in orders:
|
||||
if not (
|
||||
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
|
||||
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and filters.get("outstanding_amt_less_than")
|
||||
and not (
|
||||
flt(filters.get("outstanding_amt_greater_than"))
|
||||
<= flt(d.outstanding_amount)
|
||||
<= flt(filters.get("outstanding_amt_less_than"))
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -1526,6 +1622,8 @@ def get_negative_outstanding_invoices(
|
||||
cost_center=None,
|
||||
condition=None,
|
||||
):
|
||||
if party_type not in ["Customer", "Supplier"]:
|
||||
return []
|
||||
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
supplier_condition = ""
|
||||
if voucher_type == "Purchase Invoice":
|
||||
@@ -1574,7 +1672,7 @@ def get_negative_outstanding_invoices(
|
||||
def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
bank_account = ""
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("Invalid {0}: {1}").format(party_type, party))
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
|
||||
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
|
||||
@@ -1855,8 +1953,11 @@ def get_payment_entry(
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
100.0 + over_billing_allowance
|
||||
):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
|
||||
party_type = set_party_type(dt)
|
||||
party_account = set_party_account(dt, dn, doc, party_type)
|
||||
@@ -1917,7 +2018,12 @@ def get_payment_entry(
|
||||
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
|
||||
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
|
||||
else:
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
|
||||
if doc.doctype in (
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Purchase Order",
|
||||
"Sales Order",
|
||||
) and frappe.get_value(
|
||||
"Payment Terms Template",
|
||||
{"name": doc.payment_terms_template},
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
|
||||
@@ -999,6 +999,30 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
|
||||
self.assertTrue("is on hold" in str(err.exception).lower())
|
||||
|
||||
def test_duplicate_payment_entry_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
def test_duplicate_payment_entry_partial_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.received_amount = si.total / 2
|
||||
pe.references[0].allocated_amount = si.total / 2
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
||||
});
|
||||
}
|
||||
|
||||
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") {
|
||||
if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
|
||||
frm.add_custom_button(__('Create Payment Entry'), function(){
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",
|
||||
|
||||
@@ -254,6 +254,7 @@ class PaymentRequest(Document):
|
||||
|
||||
payment_entry.update(
|
||||
{
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
|
||||
@@ -403,25 +404,22 @@ def make_payment_request(**args):
|
||||
else ""
|
||||
)
|
||||
|
||||
existing_payment_request = None
|
||||
if args.order_type == "Shopping Cart":
|
||||
existing_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
|
||||
)
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
)
|
||||
|
||||
if existing_payment_request:
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
)
|
||||
pr = frappe.get_doc("Payment Request", existing_payment_request)
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
if args.order_type != "Shopping Cart":
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
pr.update(
|
||||
{
|
||||
@@ -472,24 +470,11 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate
|
||||
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
<<<<<<< HEAD
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
|
||||
=======
|
||||
if not ref_doc.is_pos:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
>>>>>>> 9bf87d708e (fix: `payment entry is already created` on posawesome. (#34712))
|
||||
elif dt == "POS Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
|
||||
@@ -169,21 +169,18 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t2.account_currency,
|
||||
t1.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
from `tabGL Entry` t1
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.account in (select name from `tabAccount` where report_type = 'Profit and Loss' and docstatus < 2 and company = %s)
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
dimension_fields=", ".join(dimension_fields),
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
|
||||
@@ -345,7 +345,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "POS Invoice",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1572,7 +1573,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-27 13:00:24.166684",
|
||||
"modified": "2022-09-30 03:49:50.455199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import collections
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||
from six import iteritems
|
||||
|
||||
@@ -43,6 +44,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_debit_to_acc()
|
||||
self.validate_write_off_account()
|
||||
self.validate_change_amount()
|
||||
self.validate_duplicate_serial_and_batch_no()
|
||||
self.validate_change_account()
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_warehouse()
|
||||
@@ -153,6 +155,27 @@ class POSInvoice(SalesInvoice):
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_duplicate_serial_and_batch_no(self):
|
||||
serial_nos = []
|
||||
batch_nos = []
|
||||
|
||||
for row in self.get("items"):
|
||||
if row.serial_no:
|
||||
serial_nos = row.serial_no.split("\n")
|
||||
|
||||
if row.batch_no and not row.serial_no:
|
||||
batch_nos.append(row.batch_no)
|
||||
|
||||
if serial_nos:
|
||||
for key, value in collections.Counter(serial_nos).items():
|
||||
if value > 1:
|
||||
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
|
||||
|
||||
if batch_nos:
|
||||
for key, value in collections.Counter(batch_nos).items():
|
||||
if value > 1:
|
||||
frappe.throw(_("Duplicate Batch No {0} found").format("key"))
|
||||
|
||||
def validate_pos_reserved_batch_qty(self, item):
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
|
||||
|
||||
@@ -675,18 +698,22 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql(
|
||||
"""select sum(p_item.qty) as qty
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||
where p.name = p_item.parent
|
||||
and ifnull(p.consolidated_invoice, '') = ''
|
||||
and p_item.docstatus = 1
|
||||
and p_item.item_code = %s
|
||||
and p_item.warehouse = %s
|
||||
""",
|
||||
(item_code, warehouse),
|
||||
as_dict=1,
|
||||
)
|
||||
p_inv = frappe.qb.DocType("POS Invoice")
|
||||
p_item = frappe.qb.DocType("POS Invoice Item")
|
||||
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.qty).as_("qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
& (p_inv.is_return == 0)
|
||||
& (p_item.docstatus == 1)
|
||||
& (p_item.item_code == item_code)
|
||||
& (p_item.warehouse == warehouse)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
|
||||
@@ -747,7 +774,3 @@ def add_return_modes(doc, pos_profile):
|
||||
]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("POS Invoice", ["return_against"])
|
||||
|
||||
@@ -303,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
|
||||
apply_tds: function(frm) {
|
||||
var me = this;
|
||||
|
||||
me.frm.set_value("tax_withheld_vouchers", []);
|
||||
if (!me.frm.doc.apply_tds) {
|
||||
me.frm.set_value("tax_withholding_category", '');
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", 1);
|
||||
|
||||
@@ -1580,6 +1580,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = create_pr_against_po(po.name, received_qty=4)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -872,7 +873,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-12 03:37:29.032732",
|
||||
"modified": "2023-07-02 18:39:41.495723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -316,6 +316,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
},
|
||||
|
||||
make_inter_company_invoice: function() {
|
||||
let me = this;
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
|
||||
frm: me.frm
|
||||
@@ -652,19 +653,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
}
|
||||
|
||||
// expense account
|
||||
frm.fields_dict['items'].grid.get_field('expense_account').get_query = function(doc) {
|
||||
if (erpnext.is_perpetual_inventory_enabled(doc.company)) {
|
||||
return {
|
||||
filters: {
|
||||
'report_type': 'Profit and Loss',
|
||||
'company': doc.company,
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// discount account
|
||||
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
|
||||
return {
|
||||
|
||||
@@ -1107,7 +1107,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
asset, item.base_net_amount, item.finance_book, self.posting_date
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
@@ -1122,7 +1122,7 @@ class SalesInvoice(SellingController):
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
asset, item.base_net_amount, item.finance_book, self.posting_date
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
@@ -1580,15 +1580,13 @@ class SalesInvoice(SellingController):
|
||||
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def get_returned_amount(self):
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
doc = frappe.qb.DocType(self.doctype)
|
||||
returned_amount = (
|
||||
frappe.qb.from_(doc)
|
||||
.select(Sum(doc.grand_total))
|
||||
.where(
|
||||
(doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
|
||||
)
|
||||
.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
|
||||
).run()
|
||||
|
||||
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, getdate
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
@@ -274,7 +274,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice":
|
||||
if doctype != "Sales Invoice":
|
||||
filters.update(
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
@@ -518,10 +518,19 @@ def get_invoice_total_without_tcs(inv, tax_details):
|
||||
|
||||
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
|
||||
"sum(net_total)",
|
||||
|
||||
limit_consumed = flt(
|
||||
frappe.db.get_all(
|
||||
"Purchase Invoice",
|
||||
filters={
|
||||
"supplier": ("in", parties),
|
||||
"apply_tds": 1,
|
||||
"docstatus": 1,
|
||||
"tax_withholding_category": ldc.tax_withholding_category,
|
||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
||||
},
|
||||
fields=["sum(base_net_total) as limit_consumed"],
|
||||
)[0].get("limit_consumed")
|
||||
)
|
||||
|
||||
if is_valid_certificate(
|
||||
@@ -535,10 +544,10 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - deducted_amount
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
@@ -549,9 +558,9 @@ def is_valid_certificate(
|
||||
):
|
||||
valid = False
|
||||
|
||||
if (
|
||||
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
|
||||
) and certificate_limit > deducted_amount:
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
@@ -110,9 +110,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 30000
|
||||
# Threshold calculation should be on both the invoices
|
||||
# TDS should be applied only on 1000
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
# Threshold calculation should be only on the Second invoice
|
||||
# Second didn't breach, no TDS should be applied
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
@@ -186,6 +186,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
|
||||
|
||||
# Add some tax on the order
|
||||
order.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
order.save()
|
||||
|
||||
order.apply_tds = 1
|
||||
order.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
order.submit()
|
||||
|
||||
self.assertEqual(order.taxes[0].tax_amount, 4000)
|
||||
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
||||
@@ -275,6 +311,37 @@ def cancel_invoices():
|
||||
frappe.get_doc("Sales Invoice", d).cancel()
|
||||
|
||||
|
||||
def create_purchase_order(**args):
|
||||
# return purchase order doc object
|
||||
item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name")
|
||||
args = frappe._dict(args)
|
||||
po = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Order",
|
||||
"transaction_date": today(),
|
||||
"schedule_date": today(),
|
||||
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||
"supplier": args.supplier,
|
||||
"company": "_Test Company",
|
||||
"taxes_and_charges": "",
|
||||
"currency": "INR",
|
||||
"taxes": [],
|
||||
"items": [
|
||||
{
|
||||
"doctype": "Purchase Order Item",
|
||||
"item_code": item,
|
||||
"qty": args.qty or 1,
|
||||
"rate": args.rate or 10000,
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Stock Received But Not Billed - _TC",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
po.save()
|
||||
return po
|
||||
|
||||
|
||||
def create_purchase_invoice(**args):
|
||||
# return sales invoice doc object
|
||||
item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name")
|
||||
@@ -351,6 +418,8 @@ def create_records():
|
||||
"Test TDS Supplier4",
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
"Test TDS Supplier8",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2016-04-08 14:49:58.133098",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:26.084484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciation Ledger",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciation Ledger",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2016-04-08 14:49:58.133098",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2023-06-06 09:00:07.435151",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciation Ledger",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciation Ledger",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2016-04-08 14:56:37.235981",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:18.660476",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciations and Balances",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciations and Balances",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2016-04-08 14:56:37.235981",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2023-06-06 11:33:29.611277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciations and Balances",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciations and Balances",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
|
||||
@@ -114,28 +114,6 @@ def get_assets(filters):
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
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 ds.schedule_date <= a.disposal_date then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabAsset` a, `tabDepreciation Schedule` ds
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
@@ -160,7 +138,7 @@ 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.calculate_depreciation=0 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)
|
||||
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)
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
|
||||
@@ -79,7 +79,7 @@ def get_entries(filters):
|
||||
payment_entries = frappe.db.sql(
|
||||
"""SELECT
|
||||
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||
if(paid_from=%(account)s, paid_amount * -1, received_amount)
|
||||
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
|
||||
@@ -524,11 +524,26 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions.append("cost_center in %(cost_center)s")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
|
||||
@@ -176,7 +176,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check"
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_cancelled_entries",
|
||||
|
||||
@@ -287,13 +287,23 @@ def get_conditions(filters):
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("include_default_book_entries"):
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("finance_book in (%(finance_book)s)")
|
||||
conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
if filters.get("finance_book"):
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if not filters.get("show_cancelled_entries"):
|
||||
conditions.append("is_cancelled = 0")
|
||||
|
||||
@@ -125,12 +125,14 @@ def get_revenue(data, period_list, include_in_gross=1):
|
||||
|
||||
data_to_be_removed = True
|
||||
while data_to_be_removed:
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list)
|
||||
revenue = adjust_account(revenue, period_list)
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue)
|
||||
|
||||
adjust_account_totals(revenue, period_list)
|
||||
|
||||
return copy.deepcopy(revenue)
|
||||
|
||||
|
||||
def remove_parent_with_no_child(data, period_list):
|
||||
def remove_parent_with_no_child(data):
|
||||
data_to_be_removed = False
|
||||
for parent in data:
|
||||
if "is_group" in parent and parent.get("is_group") == 1:
|
||||
@@ -147,16 +149,19 @@ def remove_parent_with_no_child(data, period_list):
|
||||
return data, data_to_be_removed
|
||||
|
||||
|
||||
def adjust_account(data, period_list, consolidated=False):
|
||||
leaf_nodes = [item for item in data if item["is_group"] == 0]
|
||||
def adjust_account_totals(data, period_list):
|
||||
totals = {}
|
||||
for node in leaf_nodes:
|
||||
set_total(node, node["total"], data, totals)
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
for d in reversed(data):
|
||||
if d.get("is_group"):
|
||||
for period in period_list:
|
||||
# reset totals for group accounts as totals set by get_data doesn't consider include_in_gross check
|
||||
d[period.key] = sum(
|
||||
item[period.key] for item in data if item.get("parent_account") == d.get("account")
|
||||
)
|
||||
else:
|
||||
set_total(d, d["total"], data, totals)
|
||||
|
||||
d["total"] = totals[d["account"]]
|
||||
|
||||
|
||||
def set_total(node, value, complete_list, totals):
|
||||
@@ -191,6 +196,9 @@ def get_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
@@ -229,6 +237,9 @@ def get_net_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
@@ -666,7 +667,7 @@ class GrossProfitGenerator(object):
|
||||
def load_invoice_items(self):
|
||||
conditions = ""
|
||||
if self.filters.company:
|
||||
conditions += " and company = %(company)s"
|
||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
||||
if self.filters.from_date:
|
||||
conditions += " and posting_date >= %(from_date)s"
|
||||
if self.filters.to_date:
|
||||
@@ -760,30 +761,30 @@ class GrossProfitGenerator(object):
|
||||
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
|
||||
"""
|
||||
|
||||
parents = []
|
||||
grouped = OrderedDict()
|
||||
|
||||
for row in self.si_list:
|
||||
if row.parent not in parents:
|
||||
parents.append(row.parent)
|
||||
# initialize list with a header row for each new parent
|
||||
grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
|
||||
row.update(
|
||||
{"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
|
||||
) # descendant rows will have indent: 1.0 or greater
|
||||
)
|
||||
|
||||
parents_index = 0
|
||||
for index, row in enumerate(self.si_list):
|
||||
if parents_index < len(parents) and row.parent == parents[parents_index]:
|
||||
invoice = self.get_invoice_row(row)
|
||||
self.si_list.insert(index, invoice)
|
||||
parents_index += 1
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
|
||||
else:
|
||||
# skipping the bundle items rows
|
||||
if not row.indent:
|
||||
row.indent = 1.0
|
||||
row.parent_invoice = row.parent
|
||||
row.invoice_or_item = row.item_code
|
||||
self.si_list.clear()
|
||||
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
self.add_bundle_items(row, index)
|
||||
for items in grouped.values():
|
||||
self.si_list.extend(items)
|
||||
|
||||
def get_invoice_row(self, row):
|
||||
# header row format
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": "",
|
||||
@@ -812,13 +813,6 @@ class GrossProfitGenerator(object):
|
||||
}
|
||||
)
|
||||
|
||||
def add_bundle_items(self, product_bundle, index):
|
||||
bundle_items = self.get_bundle_items(product_bundle)
|
||||
|
||||
for i, item in enumerate(bundle_items):
|
||||
bundle_item = self.get_bundle_item_row(product_bundle, item)
|
||||
self.si_list.insert((index + i + 1), bundle_item)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
|
||||
@@ -87,7 +87,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
"project": d.project,
|
||||
"company": d.company,
|
||||
"purchase_order": d.purchase_order,
|
||||
"purchase_receipt": d.purchase_receipt,
|
||||
"purchase_receipt": purchase_receipt,
|
||||
"expense_account": expense_account,
|
||||
"stock_qty": d.stock_qty,
|
||||
"stock_uom": d.stock_uom,
|
||||
@@ -241,7 +241,7 @@ def get_columns(additional_table_columns, filters):
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Receipt"),
|
||||
"fieldname": "Purchase Receipt",
|
||||
"fieldname": "purchase_receipt",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Receipt",
|
||||
"width": 100,
|
||||
|
||||
@@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
|
||||
@@ -157,12 +157,25 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if filters.project:
|
||||
additional_conditions += " and project = %(project)s"
|
||||
|
||||
company_fb = frappe.db.get_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if company_fb and cstr(filters.get("finance_book")) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(company_fb)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in ('') OR finance_book IS NULL)"
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
@@ -174,7 +187,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
"finance_book": filters.finance_book,
|
||||
"company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"),
|
||||
"company_fb": company_fb,
|
||||
}
|
||||
|
||||
if accounting_dimensions:
|
||||
|
||||
@@ -810,7 +810,7 @@ def get_held_invoices(party_type, party):
|
||||
|
||||
if party_type == "Supplier":
|
||||
held_invoices = frappe.db.sql(
|
||||
"select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()",
|
||||
"select name from `tabPurchase Invoice` where on_hold = 1 and release_date IS NOT NULL and release_date > CURDATE()",
|
||||
as_dict=1,
|
||||
)
|
||||
held_invoices = set(d["name"] for d in held_invoices)
|
||||
|
||||
@@ -27,6 +27,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
is_first_day_of_the_month,
|
||||
is_last_day_of_the_month,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
@@ -147,17 +148,33 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Selected Cost Center {} doesn't belongs to {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
if self.cost_center:
|
||||
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||
)
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Cost Center {} doesn't belong to Company {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
if cost_center_is_group:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
|
||||
).format(frappe.bold(self.cost_center)),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
|
||||
else:
|
||||
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
|
||||
).format(frappe.bold(self.company)),
|
||||
title=_("Missing Cost Center"),
|
||||
)
|
||||
|
||||
def validate_in_use_date(self):
|
||||
if not self.available_for_use_date:
|
||||
@@ -339,13 +356,9 @@ class Asset(AccountsController):
|
||||
if should_get_last_day:
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
from_date = self.get_from_date_for_disposal(finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
@@ -369,9 +382,9 @@ class Asset(AccountsController):
|
||||
|
||||
# For first row
|
||||
if (
|
||||
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and n == 0
|
||||
):
|
||||
from_date = add_days(
|
||||
self.available_for_use_date, -1
|
||||
@@ -383,10 +396,26 @@ class Asset(AccountsController):
|
||||
finance_book.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
# For first depr schedule date will be the start date
|
||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
if not is_first_day_of_the_month(getdate(self.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(self.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
finance_book.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
@@ -411,9 +440,7 @@ class Asset(AccountsController):
|
||||
depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book
|
||||
)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
last_schedule_date = schedule_date
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
@@ -432,7 +459,7 @@ class Asset(AccountsController):
|
||||
)
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0:
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
@@ -490,16 +517,19 @@ class Asset(AccountsController):
|
||||
for idx, s in enumerate(self.schedules, 1):
|
||||
s.idx = idx
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
def get_from_date_for_disposal(self, finance_book):
|
||||
if not self.get("schedules"):
|
||||
return self.available_for_use_date
|
||||
return add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
if len(self.finance_books) == 1:
|
||||
return self.schedules[-1].schedule_date
|
||||
|
||||
from_date = ""
|
||||
for schedule in self.get("schedules"):
|
||||
if schedule.finance_book == finance_book:
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
from_date = schedule.schedule_date
|
||||
|
||||
if from_date:
|
||||
@@ -1281,9 +1311,11 @@ def get_straight_line_or_manual_depr_amount(asset, row):
|
||||
)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
)
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
|
||||
@@ -33,7 +33,7 @@ frappe.listview_settings['Asset'] = {
|
||||
}
|
||||
},
|
||||
onload: function(me) {
|
||||
me.page.add_action_item('Make Asset Movement', function() {
|
||||
me.page.add_action_item(__("Make Asset Movement"), function() {
|
||||
const assets = me.get_checked_items();
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
@@ -32,6 +33,7 @@ def post_depreciation_entries(date=None):
|
||||
date = today()
|
||||
|
||||
failed_asset_names = []
|
||||
error_log_names = []
|
||||
|
||||
for asset_name in get_depreciable_assets(date):
|
||||
try:
|
||||
@@ -40,10 +42,12 @@ def post_depreciation_entries(date=None):
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
failed_asset_names.append(asset_name)
|
||||
error_log = frappe.log_error(e)
|
||||
error_log_names.append(error_log.name)
|
||||
|
||||
if failed_asset_names:
|
||||
set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
|
||||
notify_depr_entry_posting_error(failed_asset_names)
|
||||
notify_depr_entry_posting_error(failed_asset_names, error_log_names)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -133,16 +137,17 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
|
||||
d.db_set("journal_entry", je.name)
|
||||
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
|
||||
@@ -214,7 +219,7 @@ def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
|
||||
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed")
|
||||
|
||||
|
||||
def notify_depr_entry_posting_error(failed_asset_names):
|
||||
def notify_depr_entry_posting_error(failed_asset_names, error_log_names):
|
||||
recipients = get_users_with_role("Accounts Manager")
|
||||
|
||||
if not recipients:
|
||||
@@ -222,7 +227,8 @@ def notify_depr_entry_posting_error(failed_asset_names):
|
||||
|
||||
subject = _("Error while posting depreciation entries")
|
||||
|
||||
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
||||
asset_links = get_comma_separated_links(failed_asset_names, "Asset")
|
||||
error_log_links = get_comma_separated_links(error_log_names, "Error Log")
|
||||
|
||||
message = (
|
||||
_("Hello,")
|
||||
@@ -232,23 +238,26 @@ def notify_depr_entry_posting_error(failed_asset_names):
|
||||
)
|
||||
+ "."
|
||||
+ "<br><br>"
|
||||
+ _(
|
||||
"Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
|
||||
+ _("Here are the error logs for the aforementioned failed depreciation entries: {0}").format(
|
||||
error_log_links
|
||||
)
|
||||
+ "."
|
||||
+ "<br><br>"
|
||||
+ _("Please share this email with your support team so that they can find and fix the issue.")
|
||||
)
|
||||
|
||||
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||
|
||||
|
||||
def get_comma_separated_asset_links(asset_names):
|
||||
asset_links = []
|
||||
def get_comma_separated_links(names, doctype):
|
||||
links = []
|
||||
|
||||
for asset_name in asset_names:
|
||||
asset_links.append(get_link_to_form("Asset", asset_name))
|
||||
for name in names:
|
||||
links.append(get_link_to_form(doctype, name))
|
||||
|
||||
asset_links = ", ".join(asset_links)
|
||||
links = ", ".join(links)
|
||||
|
||||
return asset_links
|
||||
return links
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -278,7 +287,7 @@ def scrap_asset(asset_name):
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
for entry in get_gl_entries_on_asset_disposal(asset):
|
||||
for entry in get_gl_entries_on_asset_disposal(asset, date):
|
||||
entry.update({"reference_type": "Asset", "reference_name": asset_name})
|
||||
je.append("accounts", entry)
|
||||
|
||||
@@ -342,6 +351,9 @@ def modify_depreciation_schedule_for_asset_repairs(asset):
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
if not asset.calculate_depreciation:
|
||||
return
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
@@ -402,7 +414,10 @@ def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
return False
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None, date=None):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -419,23 +434,30 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
]
|
||||
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
get_profit_gl_entries(
|
||||
profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
|
||||
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None, date=None):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -452,18 +474,26 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
]
|
||||
|
||||
if accumulated_depr_amount:
|
||||
gl_entries.append(
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
)
|
||||
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
get_profit_gl_entries(
|
||||
profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
|
||||
@@ -490,7 +520,12 @@ def get_asset_details(asset, finance_book=None):
|
||||
)
|
||||
|
||||
|
||||
def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
|
||||
def get_profit_gl_entries(
|
||||
profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append(
|
||||
{
|
||||
@@ -498,6 +533,7 @@ def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciat
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
"posting_date": date,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -522,3 +558,9 @@ def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
def is_first_day_of_the_month(date):
|
||||
first_day_of_the_month = get_first_day(date)
|
||||
|
||||
return getdate(first_day_of_the_month) == getdate(date)
|
||||
|
||||
@@ -298,6 +298,79 @@ class TestAsset(AssetSetup):
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_gle_made_by_asset_sale_for_existing_asset(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-04-01",
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_depr_values = [
|
||||
["2023-03-31", 12000, 36000],
|
||||
["2024-03-31", 12000, 48000],
|
||||
["2025-03-31", 12000, 60000],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
|
||||
post_depreciation_entries(date="2023-03-31")
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
|
||||
)
|
||||
asset.load_from_db()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"_Test Fixed Asset - _TC",
|
||||
0.0,
|
||||
60000.0,
|
||||
),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
@@ -569,7 +642,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
|
||||
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in asset.get("schedules")
|
||||
@@ -613,14 +686,14 @@ class TestDepreciationMethods(AssetSetup):
|
||||
number_of_depreciations_booked=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2030-12-31",
|
||||
depreciation_start_date="2031-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=12,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
|
||||
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]]
|
||||
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
@@ -1421,7 +1494,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
@@ -1434,12 +1507,68 @@ class TestDepreciationBasics(AssetSetup):
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 99900)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_for_depreciable_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
jv.voucher_type = "Depreciation Entry"
|
||||
jv.insert()
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
d.account_type = "Depreciation"
|
||||
jv.voucher_type = "Journal Entry"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv.insert)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
|
||||
@@ -33,6 +33,7 @@ frappe.ui.form.on('Asset Category', {
|
||||
var d = locals[cdt][cdn];
|
||||
return {
|
||||
"filters": {
|
||||
"account_type": "Depreciation",
|
||||
"root_type": ["in", ["Expense", "Income"]],
|
||||
"is_group": 0,
|
||||
"company": d.company_name
|
||||
|
||||
@@ -53,7 +53,7 @@ class AssetCategory(Document):
|
||||
account_type_map = {
|
||||
"fixed_asset_account": {"account_type": ["Fixed Asset"]},
|
||||
"accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]},
|
||||
"depreciation_expense_account": {"root_type": ["Expense", "Income"]},
|
||||
"depreciation_expense_account": {"account_type": ["Depreciation"]},
|
||||
"capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]},
|
||||
}
|
||||
for d in self.accounts:
|
||||
@@ -96,7 +96,6 @@ class AssetCategory(Document):
|
||||
frappe.throw(msg, title=_("Missing Account"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_category_account(
|
||||
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
||||
):
|
||||
|
||||
@@ -63,26 +63,28 @@ frappe.ui.form.on('Asset Movement', {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 0, reqd: 1 },
|
||||
from_employee: { read_only: 0, reqd: 0 },
|
||||
to_employee: { read_only: 1, reqd: 0 }
|
||||
};
|
||||
}
|
||||
else if (frm.doc.purpose === 'Issue') {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 1, reqd: 0 },
|
||||
source_location: { read_only: 1, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 1 }
|
||||
};
|
||||
}
|
||||
Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
|
||||
let property_to_be_altered = fieldnames_to_be_altered[fieldname];
|
||||
Object.keys(property_to_be_altered).forEach(property => {
|
||||
let value = property_to_be_altered[property];
|
||||
frm.set_df_property(fieldname, property, value, cdn, 'assets');
|
||||
if (fieldnames_to_be_altered) {
|
||||
Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
|
||||
let property_to_be_altered = fieldnames_to_be_altered[fieldname];
|
||||
Object.keys(property_to_be_altered).forEach(property => {
|
||||
let value = property_to_be_altered[property];
|
||||
frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
frm.refresh_field('assets');
|
||||
frm.refresh_field('assets');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
@@ -95,10 +96,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-22 12:30:55.295670",
|
||||
"modified": "2023-06-28 16:54:26.571083",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Movement",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -148,5 +150,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -28,26 +28,20 @@ class AssetMovement(Document):
|
||||
def validate_location(self):
|
||||
for d in self.assets:
|
||||
if self.purpose in ["Transfer", "Issue"]:
|
||||
if not d.source_location:
|
||||
d.source_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
|
||||
if not d.source_location:
|
||||
frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset))
|
||||
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
|
||||
if self.purpose == "Issue":
|
||||
if d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Issuing cannot be done to a location. \
|
||||
Please enter employee who has issued Asset {0}"
|
||||
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
|
||||
).format(d.asset),
|
||||
title="Incorrect Movement Purpose",
|
||||
)
|
||||
@@ -69,28 +63,19 @@ class AssetMovement(Document):
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
# only when asset is bought and first entry is made
|
||||
if not d.source_location and not (d.target_location or d.to_employee):
|
||||
if not (d.source_location) and not (d.target_location or d.to_employee):
|
||||
frappe.throw(
|
||||
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
|
||||
)
|
||||
elif d.source_location:
|
||||
# when asset is received from an employee
|
||||
if d.target_location and not d.from_employee:
|
||||
frappe.throw(
|
||||
_("From employee is required while receiving Asset {0} to a target location").format(
|
||||
d.asset
|
||||
)
|
||||
)
|
||||
if d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
|
||||
)
|
||||
if d.to_employee and d.target_location:
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and \
|
||||
given to employee in a single movement"
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
|
||||
@@ -110,12 +95,12 @@ class AssetMovement(Document):
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_latest_location_in_asset()
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_latest_location_in_asset()
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def set_latest_location_in_asset(self):
|
||||
def set_latest_location_and_custodian_in_asset(self):
|
||||
current_location, current_employee = "", ""
|
||||
cond = "1=1"
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
if not frappe.db.exists("Location", "Test Location 2"):
|
||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
||||
|
||||
movement1 = create_asset_movement(
|
||||
create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
@@ -58,7 +58,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
|
||||
create_asset_movement(
|
||||
movement1 = create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
@@ -70,21 +70,32 @@ class TestAssetMovement(unittest.TestCase):
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
|
||||
employee = make_employee("testassetmovemp@example.com", company="_Test Company")
|
||||
create_asset_movement(
|
||||
purpose="Issue",
|
||||
company=asset.company,
|
||||
assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}],
|
||||
assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}],
|
||||
reference_doctype="Purchase Receipt",
|
||||
reference_name=pr.name,
|
||||
)
|
||||
|
||||
# after issuing asset should belong to an employee not at a location
|
||||
# after issuing, asset should belong to an employee not at a location
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
|
||||
|
||||
create_asset_movement(
|
||||
purpose="Receipt",
|
||||
company=asset.company,
|
||||
assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}],
|
||||
reference_doctype="Purchase Receipt",
|
||||
reference_name=pr.name,
|
||||
)
|
||||
|
||||
# after receiving, asset should belong to a location not at an employee
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_last_movement_cancellation(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
|
||||
|
||||
@@ -119,7 +119,9 @@ class AssetValueAdjustment(Document):
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
|
||||
total_days
|
||||
)
|
||||
from_date = self.date
|
||||
else:
|
||||
no_of_depreciations = len(
|
||||
|
||||
@@ -19,68 +19,12 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
options: "\nIn Location\nDisposed",
|
||||
default: 'In Location'
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.nowdate(),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"date_based_on",
|
||||
"label": __("Date Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Purchase Date", "Available For Use Date"],
|
||||
"default": "Purchase Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
fieldname:"asset_category",
|
||||
label: __("Asset Category"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book"
|
||||
},
|
||||
{
|
||||
fieldname:"cost_center",
|
||||
label: __("Cost Center"),
|
||||
@@ -96,9 +40,66 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"is_existing_asset",
|
||||
label: __("Is Existing Asset"),
|
||||
fieldname:"only_existing_assets",
|
||||
label: __("Only existing assets"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
{
|
||||
"fieldname": "include_default_book_assets",
|
||||
"label": __("Include Default Book Assets"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["--Select a period--", "Fiscal Year", "Date Range"],
|
||||
"default": "--Select a period--",
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.nowdate(),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
{
|
||||
"fieldname":"date_based_on",
|
||||
"label": __("Date Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Purchase Date", "Available For Use Date"],
|
||||
"default": "Purchase Date",
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, flt, formatdate, getdate
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_fiscal_year_data,
|
||||
get_period_list,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -36,87 +38,101 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("company"):
|
||||
conditions["company"] = filters.company
|
||||
|
||||
if filters.filter_based_on == "Date Range":
|
||||
if not filters.from_date and not filters.to_date:
|
||||
filters.from_date = add_months(nowdate(), -12)
|
||||
filters.to_date = nowdate()
|
||||
|
||||
conditions[date_field] = ["between", [filters.from_date, filters.to_date]]
|
||||
if filters.filter_based_on == "Fiscal Year":
|
||||
elif filters.filter_based_on == "Fiscal Year":
|
||||
if not filters.from_fiscal_year and not filters.to_fiscal_year:
|
||||
default_fiscal_year = get_fiscal_year(today())[0]
|
||||
filters.from_fiscal_year = default_fiscal_year
|
||||
filters.to_fiscal_year = default_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year)
|
||||
validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year)
|
||||
filters.year_start_date = getdate(fiscal_year.year_start_date)
|
||||
filters.year_end_date = getdate(fiscal_year.year_end_date)
|
||||
|
||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||
if filters.get("is_existing_asset"):
|
||||
conditions["is_existing_asset"] = filters.get("is_existing_asset")
|
||||
|
||||
if filters.get("only_existing_assets"):
|
||||
conditions["is_existing_asset"] = filters.get("only_existing_assets")
|
||||
if filters.get("asset_category"):
|
||||
conditions["asset_category"] = filters.get("asset_category")
|
||||
if filters.get("cost_center"):
|
||||
conditions["cost_center"] = filters.get("cost_center")
|
||||
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
# In Store assets are those that are not sold or scrapped or capitalized or decapitalized
|
||||
operand = "not in"
|
||||
if status not in "In Location":
|
||||
operand = "in"
|
||||
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped"])
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
data = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
depreciation_amount_map = get_finance_book_value_map(filters)
|
||||
pr_supplier_map = get_purchase_receipt_supplier_map()
|
||||
pi_supplier_map = get_purchase_invoice_supplier_map()
|
||||
|
||||
assets_linked_to_fb = get_assets_linked_to_fb(filters)
|
||||
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.include_default_book_assets and company_fb:
|
||||
finance_book = company_fb
|
||||
elif filters.finance_book:
|
||||
finance_book = filters.finance_book
|
||||
else:
|
||||
finance_book = None
|
||||
|
||||
depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
|
||||
|
||||
group_by = frappe.scrub(filters.get("group_by"))
|
||||
|
||||
if group_by == "asset_category":
|
||||
fields = ["asset_category", "gross_purchase_amount", "opening_accumulated_depreciation"]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
|
||||
if group_by in ("asset_category", "location"):
|
||||
data = get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map)
|
||||
return data
|
||||
|
||||
elif group_by == "location":
|
||||
fields = ["location", "gross_purchase_amount", "opening_accumulated_depreciation"]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
|
||||
|
||||
else:
|
||||
fields = [
|
||||
"name as asset_id",
|
||||
"asset_name",
|
||||
"status",
|
||||
"department",
|
||||
"company",
|
||||
"cost_center",
|
||||
"calculate_depreciation",
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
"opening_accumulated_depreciation",
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
fields = [
|
||||
"name as asset_id",
|
||||
"asset_name",
|
||||
"status",
|
||||
"department",
|
||||
"company",
|
||||
"cost_center",
|
||||
"calculate_depreciation",
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
"opening_accumulated_depreciation",
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
for asset in assets_record:
|
||||
if filters.finance_book:
|
||||
if asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
else:
|
||||
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
if (
|
||||
assets_linked_to_fb
|
||||
and asset.calculate_depreciation
|
||||
and asset.asset_id not in assets_linked_to_fb
|
||||
):
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(
|
||||
asset.asset_id, finance_book
|
||||
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
"asset_name": asset.asset_name,
|
||||
@@ -127,7 +143,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
|
||||
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -141,14 +157,23 @@ def get_data(filters):
|
||||
|
||||
def prepare_chart_data(data, filters):
|
||||
labels_values_map = {}
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
date_field = "purchase_date"
|
||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
else:
|
||||
filters_filter_based_on = filters.filter_based_on
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
filters_from_date = filters.from_date
|
||||
filters_to_date = filters.to_date
|
||||
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters.filter_based_on,
|
||||
filters_from_date,
|
||||
filters_to_date,
|
||||
filters_filter_based_on,
|
||||
"Monthly",
|
||||
company=filters.company,
|
||||
ignore_fiscal_year=True,
|
||||
@@ -172,11 +197,11 @@ def prepare_chart_data(data, filters):
|
||||
"datasets": [
|
||||
{
|
||||
"name": _("Asset Value"),
|
||||
"values": [d.get("asset_value") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
{
|
||||
"name": _("Depreciatied Amount"),
|
||||
"values": [d.get("depreciated_amount") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -185,57 +210,127 @@ def prepare_chart_data(data, filters):
|
||||
}
|
||||
|
||||
|
||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
|
||||
if asset.calculate_depreciation:
|
||||
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
else:
|
||||
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
|
||||
def get_assets_linked_to_fb(filters):
|
||||
afb = frappe.qb.DocType("Asset Finance Book")
|
||||
|
||||
return flt(depr_amount, 2)
|
||||
|
||||
|
||||
def get_finance_book_value_map(filters):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
""" Select
|
||||
parent, SUM(depreciation_amount)
|
||||
FROM `tabDepreciation Schedule`
|
||||
WHERE
|
||||
parentfield='schedules'
|
||||
AND schedule_date<=%s
|
||||
AND journal_entry IS NOT NULL
|
||||
AND ifnull(finance_book, '')=%s
|
||||
GROUP BY parent""",
|
||||
(date, cstr(filters.finance_book or "")),
|
||||
)
|
||||
query = frappe.qb.from_(afb).select(
|
||||
afb.parent,
|
||||
)
|
||||
|
||||
if filters.include_default_book_assets:
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
def get_manual_depreciation_amount_of_asset(asset, filters):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'"))
|
||||
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
|
||||
query = query.where(
|
||||
(afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (afb.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
(afb.finance_book.isin([cstr(filters.finance_book), ""])) | (afb.finance_book.isnull())
|
||||
)
|
||||
|
||||
assets_linked_to_fb = list(chain(*query.run(as_list=1)))
|
||||
|
||||
return assets_linked_to_fb
|
||||
|
||||
|
||||
def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
start_date = (
|
||||
filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date
|
||||
)
|
||||
end_date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
|
||||
result = (
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.debit))
|
||||
.where(gle.against_voucher == asset.asset_id)
|
||||
.where(gle.account == depreciation_expense_account)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.join(aca)
|
||||
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||
.join(company)
|
||||
.on(company.name == asset.company)
|
||||
.select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount"))
|
||||
.where(
|
||||
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.posting_date <= date)
|
||||
).run()
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
)
|
||||
|
||||
if result and result[0] and result[0][0]:
|
||||
depr_amount = result[0][0]
|
||||
if filters.only_existing_assets:
|
||||
query = query.where(asset.is_existing_asset == 1)
|
||||
if filters.asset_category:
|
||||
query = query.where(asset.asset_category == filters.asset_category)
|
||||
if filters.cost_center:
|
||||
query = query.where(asset.cost_center == filters.cost_center)
|
||||
if filters.status:
|
||||
if filters.status == "In Location":
|
||||
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
else:
|
||||
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
if finance_book:
|
||||
query = query.where(
|
||||
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
depr_amount = 0
|
||||
query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
|
||||
if filters.filter_based_on in ("Date Range", "Fiscal Year"):
|
||||
query = query.where(gle.posting_date >= start_date)
|
||||
query = query.where(gle.posting_date <= end_date)
|
||||
|
||||
return depr_amount
|
||||
query = query.groupby(asset.name)
|
||||
|
||||
asset_depr_amount_map = query.run()
|
||||
|
||||
return dict(asset_depr_amount_map)
|
||||
|
||||
|
||||
def get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map):
|
||||
fields = [
|
||||
group_by,
|
||||
"name",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"calculate_depreciation",
|
||||
]
|
||||
assets = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
data = []
|
||||
|
||||
for a in assets:
|
||||
if assets_linked_to_fb and a.calculate_depreciation and a.name not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0)
|
||||
a["asset_value"] = (
|
||||
a["gross_purchase_amount"] - a["opening_accumulated_depreciation"] - a["depreciated_amount"]
|
||||
)
|
||||
|
||||
del a["name"]
|
||||
del a["calculate_depreciation"]
|
||||
|
||||
idx = ([i for i, d in enumerate(data) if a[group_by] == d[group_by]] or [None])[0]
|
||||
if idx is None:
|
||||
data.append(a)
|
||||
else:
|
||||
for field in (
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"depreciated_amount",
|
||||
"asset_value",
|
||||
):
|
||||
data[idx][field] = data[idx][field] + a[field]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_purchase_receipt_supplier_map():
|
||||
@@ -276,41 +371,41 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"fieldname": frappe.scrub(filters.get("group_by")),
|
||||
"options": filters.get("group_by"),
|
||||
"width": 120,
|
||||
"width": 216,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 90,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
"label": _("Asset Id"),
|
||||
"label": _("Asset ID"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "asset_id",
|
||||
"options": "Asset",
|
||||
|
||||
@@ -199,7 +199,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
);
|
||||
}
|
||||
|
||||
if(flt(doc.per_billed)==0) {
|
||||
if(flt(doc.per_billed) < 100) {
|
||||
this.frm.add_custom_button(__('Payment Request'),
|
||||
function() { me.make_payment_request() }, __('Create'));
|
||||
}
|
||||
@@ -242,7 +242,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
source_name: this.frm.doc.supplier,
|
||||
target: this.frm,
|
||||
setters: {
|
||||
company: me.frm.doc.company
|
||||
company: this.frm.doc.company
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: ["!=", 2],
|
||||
|
||||
@@ -84,7 +84,7 @@ def get_data(conditions, filters):
|
||||
and po.docstatus = 1
|
||||
{0}
|
||||
GROUP BY poi.name
|
||||
ORDER BY po.transaction_date ASC
|
||||
ORDER BY po.transaction_date ASC, poi.item_code ASC
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe import _, bold, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
@@ -256,8 +256,8 @@ class AccountsController(TransactionBase):
|
||||
self.validate_payment_schedule_dates()
|
||||
self.set_due_date()
|
||||
self.set_payment_schedule()
|
||||
self.validate_payment_schedule_amount()
|
||||
if not self.get("ignore_default_payment_terms_template"):
|
||||
self.validate_payment_schedule_amount()
|
||||
self.validate_due_date()
|
||||
self.validate_advance_entries()
|
||||
|
||||
@@ -388,6 +388,15 @@ class AccountsController(TransactionBase):
|
||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||
|
||||
label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
|
||||
|
||||
field = frappe.scrub(label)
|
||||
|
||||
for row in self.get("items"):
|
||||
if not row.get(field):
|
||||
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
|
||||
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
|
||||
|
||||
def disable_pricing_rule_on_internal_transfer(self):
|
||||
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||
self.ignore_pricing_rule = 1
|
||||
@@ -882,6 +891,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return is_inclusive
|
||||
|
||||
def should_show_taxes_as_table_in_print(self):
|
||||
return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
@@ -1577,6 +1589,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
|
||||
grand_total = self.get("rounded_total") or self.grand_total
|
||||
automatically_fetch_payment_terms = 0
|
||||
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
||||
@@ -1622,19 +1635,28 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
self.append("payment_schedule", data)
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not (
|
||||
automatically_fetch_payment_terms
|
||||
and allocate_payment_based_on_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
|
||||
def get_order_details(self):
|
||||
if self.doctype == "Sales Invoice":
|
||||
@@ -1687,6 +1709,10 @@ 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 schedule.discount_type == "Percentage":
|
||||
|
||||
@@ -30,10 +30,16 @@ def set_print_templates_for_taxes(doc, settings):
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"total": "templates/print_formats/includes/total.html",
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
if not doc.should_show_taxes_as_table_in_print():
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_columns(display_columns, compact_fields):
|
||||
compact_fields = compact_fields + ["image", "item_code", "item_name"]
|
||||
|
||||
@@ -678,7 +678,7 @@ class StockController(AccountsController):
|
||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
return message
|
||||
|
||||
def repost_future_sle_and_gle(self):
|
||||
def repost_future_sle_and_gle(self, force=False):
|
||||
args = frappe._dict(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
@@ -689,7 +689,10 @@ class StockController(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
if future_sle_exists(args) or repost_required_for_queue(self):
|
||||
if self.docstatus == 2:
|
||||
force = True
|
||||
|
||||
if force or future_sle_exists(args) or repost_required_for_queue(self):
|
||||
item_based_reposting = cint(
|
||||
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||
)
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -234,7 +235,8 @@
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"label": "Brand",
|
||||
"options": "Brand"
|
||||
"options": "Brand",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -346,7 +348,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-13 04:05:11.614087",
|
||||
"modified": "2022-09-30 04:01:52.090732",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
|
||||
@@ -409,9 +409,6 @@ def on_doctype_update():
|
||||
# since route is a Text column, it needs a length for indexing
|
||||
frappe.db.add_index("Website Item", ["route(500)"])
|
||||
|
||||
frappe.db.add_index("Website Item", ["item_group"])
|
||||
frappe.db.add_index("Website Item", ["brand"])
|
||||
|
||||
|
||||
def check_if_user_is_customer(user=None):
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
|
||||
@@ -78,9 +78,10 @@ erpnext.ProductList = class {
|
||||
let title_html = `<div style="display: flex; margin-left: -15px;">`;
|
||||
title_html += `
|
||||
<div class="col-8" style="margin-right: -15px;">
|
||||
<a class="" href="/${ item.route || '#' }"
|
||||
style="color: var(--gray-800); font-weight: 500;">
|
||||
<a href="/${ item.route || '#' }">
|
||||
<div class="product-title">
|
||||
${ title }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@@ -201,4 +202,4 @@ erpnext.ProductList = class {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"fieldname": "slide_3_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -214,6 +215,7 @@
|
||||
"fieldname": "slide_4_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -263,6 +265,7 @@
|
||||
"fieldname": "slide_5_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -274,7 +277,7 @@
|
||||
}
|
||||
],
|
||||
"idx": 2,
|
||||
"modified": "2021-02-24 15:57:05.889709",
|
||||
"modified": "2023-05-12 15:03:57.604060",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Hero Slider",
|
||||
|
||||
@@ -108,8 +108,8 @@ class EmployeeAdvance(Document):
|
||||
EmployeeAdvanceOverPayment,
|
||||
)
|
||||
|
||||
if flt(return_amount) > self.paid_amount - self.claimed_amount:
|
||||
frappe.throw(_("Return amount cannot be greater unclaimed amount"))
|
||||
if flt(return_amount) > 0 and flt(return_amount) > (self.paid_amount - self.claimed_amount):
|
||||
frappe.throw(_("Return amount cannot be greater than unclaimed amount"))
|
||||
|
||||
self.db_set("paid_amount", paid_amount)
|
||||
self.db_set("return_amount", return_amount)
|
||||
|
||||
@@ -333,7 +333,6 @@ def get_leave_allocation_for_period(
|
||||
).run()[0][0] or 0.0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
|
||||
"""Returns carry forwarded leaves for the given employee"""
|
||||
unused_leaves = 0.0
|
||||
|
||||
@@ -8,7 +8,15 @@ from math import ceil
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
|
||||
from frappe.utils import (
|
||||
comma_and,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
)
|
||||
from six import string_types
|
||||
|
||||
|
||||
@@ -66,7 +74,6 @@ class LeavePolicyAssignment(Document):
|
||||
).format(frappe.bold(get_link_to_form("Leave Type", leave_type.name)))
|
||||
frappe.msgprint(msg, indicator="orange", alert=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def grant_leave_alloc_for_employee(self):
|
||||
if self.leaves_allocated:
|
||||
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
|
||||
@@ -192,9 +199,9 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj
|
||||
date = getdate(frappe.flags.current_date) or getdate()
|
||||
|
||||
if based_on_doj:
|
||||
# if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ,
|
||||
# if leave type allocation is based on DOJ, and the date of assignment creation is after DOJ,
|
||||
# then the month should be considered
|
||||
if date.day == date_of_joining.day:
|
||||
if date.day >= date_of_joining.day:
|
||||
months_passed += 1
|
||||
else:
|
||||
last_day_of_month = get_last_day(date)
|
||||
@@ -207,7 +214,6 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_assignment_for_multiple_employees(employees, data):
|
||||
|
||||
if isinstance(employees, string_types):
|
||||
employees = json.loads(employees)
|
||||
|
||||
@@ -215,6 +221,8 @@ def create_assignment_for_multiple_employees(employees, data):
|
||||
data = frappe._dict(json.loads(data))
|
||||
|
||||
docs_name = []
|
||||
failed = []
|
||||
|
||||
for employee in employees:
|
||||
assignment = frappe.new_doc("Leave Policy Assignment")
|
||||
assignment.employee = employee
|
||||
@@ -225,18 +233,45 @@ def create_assignment_for_multiple_employees(employees, data):
|
||||
assignment.leave_period = data.leave_period or None
|
||||
assignment.carry_forward = data.carry_forward
|
||||
assignment.save()
|
||||
try:
|
||||
assignment.submit()
|
||||
except frappe.exceptions.ValidationError:
|
||||
continue
|
||||
|
||||
frappe.db.commit()
|
||||
savepoint = "before_assignment_submission"
|
||||
|
||||
try:
|
||||
frappe.db.savepoint(savepoint)
|
||||
assignment.submit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.log_error(title=f"Leave Policy Assignment submission failed for {assignment.name}")
|
||||
failed.append(assignment.name)
|
||||
|
||||
docs_name.append(assignment.name)
|
||||
|
||||
if failed:
|
||||
show_assignment_submission_status(failed)
|
||||
|
||||
return docs_name
|
||||
|
||||
|
||||
def show_assignment_submission_status(failed):
|
||||
frappe.clear_messages()
|
||||
assignment_list = [get_link_to_form("Leave Policy Assignment", entry) for entry in failed]
|
||||
|
||||
msg = _("Failed to submit some leave policy assignments:")
|
||||
msg += " " + comma_and(assignment_list, False) + "<hr>"
|
||||
msg += (
|
||||
_("Check {0} for more details")
|
||||
.format("<a href='/app/List/Error Log?reference_doctype=Leave Policy Assignment'>{0}</a>")
|
||||
.format(_("Error Log"))
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
msg,
|
||||
indicator="red",
|
||||
title=_("Submission Failed"),
|
||||
is_minimizable=True,
|
||||
)
|
||||
|
||||
|
||||
def get_leave_type_details():
|
||||
leave_type_details = frappe._dict()
|
||||
leave_types = frappe.get_all(
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
get_query() {
|
||||
let filters = {"is_active": 1};
|
||||
if (cur_dialog.fields_dict.company.value)
|
||||
filters["company"] = cur_dialog.fields_dict.company.value;
|
||||
filters["company"] = cur_dialog?.fields_dict?.company?.value;
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Count
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -15,31 +16,34 @@ def get_children(parent=None, company=None, exclude_node=None):
|
||||
if exclude_node:
|
||||
filters.append(["name", "!=", exclude_node])
|
||||
|
||||
employees = frappe.get_list(
|
||||
employees = frappe.get_all(
|
||||
"Employee",
|
||||
fields=["employee_name as name", "name as id", "reports_to", "image", "designation as title"],
|
||||
fields=[
|
||||
"employee_name as name",
|
||||
"name as id",
|
||||
"lft",
|
||||
"rgt",
|
||||
"reports_to",
|
||||
"image",
|
||||
"designation as title",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
for employee in employees:
|
||||
is_expandable = frappe.db.count("Employee", filters={"reports_to": employee.get("id")})
|
||||
employee.connections = get_connections(employee.id)
|
||||
employee.expandable = 1 if is_expandable else 0
|
||||
employee.connections = get_connections(employee.id, employee.lft, employee.rgt)
|
||||
employee.expandable = bool(employee.connections)
|
||||
|
||||
return employees
|
||||
|
||||
|
||||
def get_connections(employee):
|
||||
num_connections = 0
|
||||
def get_connections(employee: str, lft: int, rgt: int) -> int:
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
query = (
|
||||
frappe.qb.from_(Employee)
|
||||
.select(Count(Employee.name))
|
||||
.where((Employee.lft > lft) & (Employee.rgt < rgt))
|
||||
).run()
|
||||
|
||||
nodes_to_expand = frappe.get_list("Employee", filters=[["reports_to", "=", employee]])
|
||||
num_connections += len(nodes_to_expand)
|
||||
|
||||
while nodes_to_expand:
|
||||
parent = nodes_to_expand.pop(0)
|
||||
descendants = frappe.get_list("Employee", filters=[["reports_to", "=", parent.name]])
|
||||
num_connections += len(descendants)
|
||||
nodes_to_expand.extend(descendants)
|
||||
|
||||
return num_connections
|
||||
return query[0][0]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.page.organizational_chart.organizational_chart import get_children
|
||||
|
||||
|
||||
class TestOrganizationalChart(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.company = create_company("Test Org Chart").name
|
||||
frappe.db.delete("Employee", {"company": self.company})
|
||||
|
||||
def test_get_children(self):
|
||||
company = create_company("Test Org Chart").name
|
||||
emp1 = make_employee("testemp1@mail.com", company=self.company)
|
||||
emp2 = make_employee("testemp2@mail.com", company=self.company, reports_to=emp1)
|
||||
emp3 = make_employee("testemp3@mail.com", company=self.company, reports_to=emp1)
|
||||
make_employee("testemp4@mail.com", company=self.company, reports_to=emp2)
|
||||
|
||||
# root node
|
||||
children = get_children(company=self.company)
|
||||
self.assertEqual(len(children), 1)
|
||||
self.assertEqual(children[0].id, emp1)
|
||||
self.assertEqual(children[0].connections, 3)
|
||||
|
||||
# root's children
|
||||
children = get_children(parent=emp1, company=self.company)
|
||||
self.assertEqual(len(children), 2)
|
||||
self.assertEqual(children[0].id, emp2)
|
||||
self.assertEqual(children[0].connections, 1)
|
||||
self.assertEqual(children[1].id, emp3)
|
||||
self.assertEqual(children[1].connections, 0)
|
||||
|
||||
|
||||
def create_company(name):
|
||||
if frappe.db.exists("Company", name):
|
||||
return frappe.get_doc("Company", name)
|
||||
|
||||
company = frappe.new_doc("Company")
|
||||
company.update(
|
||||
{
|
||||
"company_name": name,
|
||||
"default_currency": "USD",
|
||||
"country": "United States",
|
||||
}
|
||||
)
|
||||
return company.insert()
|
||||
@@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
interest = per_day_interest * 15
|
||||
|
||||
self.assertEqual(amounts["pending_principal_amount"], 1500000)
|
||||
self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))
|
||||
|
||||
@@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController):
|
||||
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
|
||||
|
||||
if not self.last_accrual_date:
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan)
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
@@ -274,14 +274,14 @@ def make_loan_interest_accrual_entry(args):
|
||||
|
||||
|
||||
def get_no_of_days_for_interest_accural(loan, posting_date):
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name)
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
|
||||
|
||||
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
|
||||
|
||||
return no_of_days
|
||||
|
||||
|
||||
def get_last_accrual_date(loan):
|
||||
def get_last_accrual_date(loan, posting_date):
|
||||
last_posting_date = frappe.db.sql(
|
||||
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
|
||||
WHERE loan = %s and docstatus = 1""",
|
||||
@@ -289,12 +289,30 @@ def get_last_accrual_date(loan):
|
||||
)
|
||||
|
||||
if last_posting_date[0][0]:
|
||||
last_interest_accrual_date = last_posting_date[0][0]
|
||||
# interest for last interest accrual date is already booked, so add 1 day
|
||||
return add_days(last_posting_date[0][0], 1)
|
||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > add_days(
|
||||
getdate(last_interest_accrual_date), 1
|
||||
):
|
||||
last_interest_accrual_date = last_disbursement_date
|
||||
|
||||
return add_days(last_interest_accrual_date, 1)
|
||||
else:
|
||||
return frappe.db.get_value("Loan", loan, "disbursement_date")
|
||||
|
||||
|
||||
def get_last_disbursement_date(loan, posting_date):
|
||||
last_disbursement_date = frappe.db.get_value(
|
||||
"Loan Disbursement",
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
|
||||
"MAX(posting_date)",
|
||||
)
|
||||
|
||||
return last_disbursement_date
|
||||
|
||||
|
||||
def days_in_year(year):
|
||||
days = 365
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class LoanRepayment(AccountsController):
|
||||
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
|
||||
if not self.is_term_loan:
|
||||
# get last loan interest accrual date
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan)
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
|
||||
|
||||
# get posting date upto which interest has to be accrued
|
||||
per_day_interest = get_per_day_interest(
|
||||
@@ -250,6 +250,9 @@ class LoanRepayment(AccountsController):
|
||||
)
|
||||
|
||||
def check_future_accruals(self):
|
||||
if self.is_term_loan:
|
||||
return
|
||||
|
||||
future_accrual_date = frappe.db.get_value(
|
||||
"Loan Interest Accrual",
|
||||
{"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan},
|
||||
@@ -724,7 +727,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
||||
if due_date:
|
||||
pending_days = date_diff(posting_date, due_date) + 1
|
||||
else:
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name)
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
|
||||
pending_days = date_diff(posting_date, last_accrual_date) + 1
|
||||
|
||||
if pending_days > 0:
|
||||
|
||||
@@ -48,7 +48,8 @@ frappe.ui.form.on("BOM", {
|
||||
return {
|
||||
query: "erpnext.manufacturing.doctype.bom.bom.item_query",
|
||||
filters: {
|
||||
"item_code": doc.item
|
||||
"include_item_in_manufacturing": 1,
|
||||
"is_fixed_asset": 0
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1346,8 +1346,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not has_variants:
|
||||
query_filters["has_variants"] = 0
|
||||
|
||||
if filters and filters.get("is_stock_item"):
|
||||
query_filters["is_stock_item"] = 1
|
||||
if filters:
|
||||
for fieldname, value in filters.items():
|
||||
query_filters[fieldname] = value
|
||||
|
||||
return frappe.get_list(
|
||||
"Item",
|
||||
|
||||
@@ -605,6 +605,45 @@ class TestBOM(FrappeTestCase):
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
def test_do_not_include_manufacturing_and_fixed_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import item_query
|
||||
|
||||
if not frappe.db.exists("Asset Category", "Computers-Test"):
|
||||
doc = frappe.get_doc({"doctype": "Asset Category", "asset_category_name": "Computers-Test"})
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
|
||||
for item_code, properties in {
|
||||
"_Test RM Item 1 Do Not Include In Manufacture": {
|
||||
"is_stock_item": 1,
|
||||
"include_item_in_manufacturing": 0,
|
||||
},
|
||||
"_Test RM Item 2 Fixed Asset Item": {
|
||||
"is_fixed_asset": 1,
|
||||
"is_stock_item": 0,
|
||||
"asset_category": "Computers-Test",
|
||||
},
|
||||
"_Test RM Item 3 Manufacture Item": {"is_stock_item": 1, "include_item_in_manufacturing": 1},
|
||||
}.items():
|
||||
make_item(item_code, properties)
|
||||
|
||||
data = item_query(
|
||||
"Item",
|
||||
txt="_Test RM Item",
|
||||
searchfield="name",
|
||||
start=0,
|
||||
page_len=20000,
|
||||
filters={"include_item_in_manufacturing": 1, "is_fixed_asset": 0},
|
||||
)
|
||||
|
||||
items = []
|
||||
for row in data:
|
||||
items.append(row[0])
|
||||
|
||||
self.assertTrue("_Test RM Item 1 Do Not Include In Manufacture" not in items)
|
||||
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
|
||||
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -79,6 +79,7 @@ class BOMUpdateLog(Document):
|
||||
else:
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
queue="long",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
)
|
||||
|
||||
@@ -157,12 +157,21 @@ def get_next_higher_level_boms(
|
||||
def get_leaf_boms() -> List[str]:
|
||||
"Get BOMs that have no dependencies."
|
||||
|
||||
return frappe.db.sql_list(
|
||||
"""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1
|
||||
and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
||||
)
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType("BOM Item")
|
||||
|
||||
boms = (
|
||||
frappe.qb.from_(bom)
|
||||
.left_join(bom_item)
|
||||
.on((bom.name == bom_item.parent) & (bom_item.bom_no != ""))
|
||||
.select(bom.name)
|
||||
.where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull()))
|
||||
.distinct()
|
||||
).run(as_list=True)
|
||||
|
||||
boms = [bom[0] for bom in boms]
|
||||
|
||||
return boms
|
||||
|
||||
|
||||
def _generate_dependence_map() -> defaultdict:
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-12 10:15:06.572401",
|
||||
"modified": "2023-05-22 23:26:57.589331",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -657,7 +657,7 @@ class JobCard(Document):
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if self.docstatus < 2:
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||
self.status = "Material Transferred"
|
||||
|
||||
if self.time_logs:
|
||||
|
||||
@@ -675,10 +675,9 @@ class ProductionPlan(Document):
|
||||
material_request.flags.ignore_permissions = 1
|
||||
material_request.run_method("set_missing_values")
|
||||
|
||||
material_request.save()
|
||||
if self.get("submit_material_request"):
|
||||
material_request.submit()
|
||||
else:
|
||||
material_request.save()
|
||||
|
||||
frappe.flags.mute_messages = False
|
||||
|
||||
|
||||
@@ -592,20 +592,18 @@ erpnext.work_order = {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
} else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
|
||||
let allowance_percentage = frm.doc.__onload.overproduction_percentage;
|
||||
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -69,7 +69,7 @@ def get_columns(filters):
|
||||
"label": _("Id"),
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"options": "Work Order",
|
||||
"options": "Quality Inspection",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150},
|
||||
|
||||
@@ -7,4 +7,6 @@ def execute():
|
||||
frappe.reload_doc("manufacturing", "doctype", "work_order")
|
||||
frappe.reload_doc("manufacturing", "doctype", "work_order_item")
|
||||
|
||||
frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabWork Order Item` SET amount = ifnull(rate, 0.0) * ifnull(required_qty, 0.0)"""
|
||||
)
|
||||
|
||||
@@ -145,7 +145,6 @@ class AdditionalSalary(Document):
|
||||
return amount_per_day * no_of_days
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_additional_salaries(employee, start_date, end_date, component_type):
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"depends_on": "allow_tax_exemption",
|
||||
"fieldname": "standard_tax_exemption_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Standard Tax Exemption Amount",
|
||||
@@ -104,7 +103,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-31 22:42:08.139520",
|
||||
"modified": "2023-05-01 13:42:08.139520",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Income Tax Slab",
|
||||
|
||||
@@ -80,22 +80,27 @@ frappe.ui.form.on("Salary Slip", {
|
||||
},
|
||||
|
||||
currency: function(frm) {
|
||||
frm.trigger("update_currency_changes");
|
||||
},
|
||||
|
||||
update_currency_changes: function(frm) {
|
||||
frm.trigger("set_exchange_rate");
|
||||
frm.trigger("set_dynamic_labels");
|
||||
},
|
||||
|
||||
set_dynamic_labels: function(frm) {
|
||||
var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency");
|
||||
if (frm.doc.employee && frm.doc.currency) {
|
||||
frappe.run_serially([
|
||||
() => frm.events.set_exchange_rate(frm, company_currency),
|
||||
() => frm.events.change_form_labels(frm, company_currency),
|
||||
() => frm.events.change_grid_labels(frm),
|
||||
() => frm.refresh_fields()
|
||||
() => frm.events.change_form_labels(frm),
|
||||
() => frm.events.change_grid_labels(frm),
|
||||
() => frm.refresh_fields()
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
set_exchange_rate: function(frm, company_currency) {
|
||||
set_exchange_rate: function(frm) {
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (frm.doc.currency) {
|
||||
var from_currency = frm.doc.currency;
|
||||
@@ -133,9 +138,11 @@ frappe.ui.form.on("Salary Slip", {
|
||||
frm.set_df_property('section_break_43', 'hidden', 1);
|
||||
},
|
||||
|
||||
change_form_labels: function(frm, company_currency) {
|
||||
change_form_labels: function(frm) {
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
|
||||
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
|
||||
"base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date", "gross_base_year_to_date"],
|
||||
"base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date", "base_gross_year_to_date"],
|
||||
company_currency);
|
||||
|
||||
frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date", "gross_year_to_date"],
|
||||
@@ -207,6 +214,9 @@ frappe.ui.form.on("Salary Slip", {
|
||||
frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as {0}. You can can change this in {1}", [r.message, frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)]));
|
||||
}
|
||||
frm.refresh();
|
||||
// triggering events explicitly because structure is set on the server-side
|
||||
// and currency is fetched from the structure
|
||||
frm.trigger("update_currency_changes");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import datetime
|
||||
import math
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
ceil,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
floor,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
@@ -45,6 +46,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import (
|
||||
get_payroll_period,
|
||||
get_period_factor,
|
||||
)
|
||||
from erpnext.payroll.utils import prepare_error_msg, sanitize_expression
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
|
||||
@@ -57,8 +59,10 @@ class SalarySlip(TransactionBase):
|
||||
"float": float,
|
||||
"long": int,
|
||||
"round": round,
|
||||
"date": datetime.date,
|
||||
"date": date,
|
||||
"getdate": getdate,
|
||||
"ceil": ceil,
|
||||
"floor": floor,
|
||||
}
|
||||
|
||||
def autoname(self):
|
||||
@@ -653,15 +657,17 @@ class SalarySlip(TransactionBase):
|
||||
amount = self.eval_condition_and_formula(struct_row, data)
|
||||
|
||||
if struct_row.statistical_component:
|
||||
default_data[struct_row.abbr] = amount
|
||||
|
||||
# update statitical component amount in reference data based on payment days
|
||||
# since row for statistical component is not added to salary slip
|
||||
if struct_row.depends_on_payment_days:
|
||||
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||
default_data[struct_row.abbr] = amount
|
||||
data[struct_row.abbr] = flt(
|
||||
(flt(amount) * flt(self.payment_days) / cint(self.total_working_days)),
|
||||
struct_row.precision("amount"),
|
||||
payment_days_amount = (
|
||||
flt(amount) * flt(self.payment_days) / cint(self.total_working_days)
|
||||
if self.total_working_days
|
||||
else 0
|
||||
)
|
||||
data[struct_row.abbr] = payment_days_amount
|
||||
|
||||
elif amount or struct_row.amount_based_on_formula and amount is not None:
|
||||
default_amount = self.eval_condition_and_formula(struct_row, default_data)
|
||||
@@ -721,32 +727,53 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
return data, default_data
|
||||
|
||||
def eval_condition_and_formula(self, d, data):
|
||||
def eval_condition_and_formula(self, struct_row, data):
|
||||
try:
|
||||
condition = d.condition.strip().replace("\n", " ") if d.condition else None
|
||||
condition = sanitize_expression(struct_row.condition)
|
||||
if condition:
|
||||
if not frappe.safe_eval(condition, self.whitelisted_globals, data):
|
||||
return None
|
||||
amount = d.amount
|
||||
if d.amount_based_on_formula:
|
||||
formula = d.formula.strip().replace("\n", " ") if d.formula else None
|
||||
amount = struct_row.amount
|
||||
if struct_row.amount_based_on_formula:
|
||||
formula = sanitize_expression(struct_row.formula)
|
||||
if formula:
|
||||
amount = flt(frappe.safe_eval(formula, self.whitelisted_globals, data), d.precision("amount"))
|
||||
amount = flt(
|
||||
frappe.safe_eval(formula, self.whitelisted_globals, data), struct_row.precision("amount")
|
||||
)
|
||||
if amount:
|
||||
data[d.abbr] = amount
|
||||
data[struct_row.abbr] = amount
|
||||
|
||||
return amount
|
||||
|
||||
except NameError as err:
|
||||
frappe.throw(
|
||||
_("{0} <br> This error can be due to missing or deleted field.").format(err),
|
||||
title=_("Name error"),
|
||||
except NameError as ne:
|
||||
message = prepare_error_msg(
|
||||
row=struct_row,
|
||||
error=ne,
|
||||
expression=formula or condition,
|
||||
description=_("This error can be due to missing or deleted field."),
|
||||
)
|
||||
except SyntaxError as err:
|
||||
frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
|
||||
|
||||
frappe.throw(message, title=_("Name error"))
|
||||
|
||||
except SyntaxError as se:
|
||||
message = prepare_error_msg(
|
||||
row=struct_row,
|
||||
error=se,
|
||||
expression=formula or condition,
|
||||
description=_("Please check the syntax of your formula."),
|
||||
)
|
||||
|
||||
frappe.throw(message, title=_("Syntax error"))
|
||||
|
||||
except Exception as e:
|
||||
frappe.throw(_("Error in formula or condition: {0}").format(e))
|
||||
raise
|
||||
message = prepare_error_msg(
|
||||
row=struct_row,
|
||||
error=e,
|
||||
expression=formula or condition,
|
||||
description=_("This error can be due to invalid formula or condition."),
|
||||
)
|
||||
|
||||
frappe.throw(message, title=_("Error in formula or condition"))
|
||||
|
||||
def add_employee_benefits(self, payroll_period):
|
||||
for struct_row in self._salary_structure_doc.get("earnings"):
|
||||
@@ -957,7 +984,7 @@ class SalarySlip(TransactionBase):
|
||||
tax_slab.allow_tax_exemption, payroll_period=payroll_period
|
||||
)
|
||||
future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (
|
||||
math.ceil(remaining_sub_periods) - 1
|
||||
ceil(remaining_sub_periods) - 1
|
||||
)
|
||||
|
||||
# get taxable_earnings, addition_earnings for current actual payment days
|
||||
@@ -1331,6 +1358,7 @@ class SalarySlip(TransactionBase):
|
||||
if declaration:
|
||||
total_exemption_amount = declaration
|
||||
|
||||
if tax_slab.standard_tax_exemption_amount:
|
||||
total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount)
|
||||
|
||||
return total_exemption_amount
|
||||
|
||||
@@ -8,222 +8,325 @@ from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
|
||||
salary_slip = frappe.qb.DocType("Salary Slip")
|
||||
salary_detail = frappe.qb.DocType("Salary Detail")
|
||||
salary_component = frappe.qb.DocType("Salary Component")
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
currency = None
|
||||
if filters.get("currency"):
|
||||
currency = filters.get("currency")
|
||||
company_currency = erpnext.get_company_currency(filters.get("company"))
|
||||
|
||||
salary_slips = get_salary_slips(filters, company_currency)
|
||||
if not salary_slips:
|
||||
return [], []
|
||||
|
||||
columns, earning_types, ded_types = get_columns(salary_slips)
|
||||
ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency)
|
||||
ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency)
|
||||
earning_types, ded_types = get_earning_and_deduction_types(salary_slips)
|
||||
columns = get_columns(earning_types, ded_types)
|
||||
|
||||
ss_earning_map = get_salary_slip_details(salary_slips, currency, company_currency, "earnings")
|
||||
ss_ded_map = get_salary_slip_details(salary_slips, currency, company_currency, "deductions")
|
||||
|
||||
doj_map = get_employee_doj_map()
|
||||
|
||||
data = []
|
||||
for ss in salary_slips:
|
||||
row = [
|
||||
ss.name,
|
||||
ss.employee,
|
||||
ss.employee_name,
|
||||
doj_map.get(ss.employee),
|
||||
ss.branch,
|
||||
ss.department,
|
||||
ss.designation,
|
||||
ss.company,
|
||||
ss.start_date,
|
||||
ss.end_date,
|
||||
ss.leave_without_pay,
|
||||
ss.payment_days,
|
||||
]
|
||||
row = {
|
||||
"salary_slip_id": ss.name,
|
||||
"employee": ss.employee,
|
||||
"employee_name": ss.employee_name,
|
||||
"data_of_joining": doj_map.get(ss.employee),
|
||||
"branch": ss.branch,
|
||||
"department": ss.department,
|
||||
"designation": ss.designation,
|
||||
"company": ss.company,
|
||||
"start_date": ss.start_date,
|
||||
"end_date": ss.end_date,
|
||||
"leave_without_pay": ss.leave_without_pay,
|
||||
"payment_days": ss.payment_days,
|
||||
"currency": currency or company_currency,
|
||||
"total_loan_repayment": ss.total_loan_repayment,
|
||||
}
|
||||
|
||||
if ss.branch is not None:
|
||||
columns[3] = columns[3].replace("-1", "120")
|
||||
if ss.department is not None:
|
||||
columns[4] = columns[4].replace("-1", "120")
|
||||
if ss.designation is not None:
|
||||
columns[5] = columns[5].replace("-1", "120")
|
||||
if ss.leave_without_pay is not None:
|
||||
columns[9] = columns[9].replace("-1", "130")
|
||||
update_column_width(ss, columns)
|
||||
|
||||
for e in earning_types:
|
||||
row.append(ss_earning_map.get(ss.name, {}).get(e))
|
||||
|
||||
if currency == company_currency:
|
||||
row += [flt(ss.gross_pay) * flt(ss.exchange_rate)]
|
||||
else:
|
||||
row += [ss.gross_pay]
|
||||
row.update({frappe.scrub(e): ss_earning_map.get(ss.name, {}).get(e)})
|
||||
|
||||
for d in ded_types:
|
||||
row.append(ss_ded_map.get(ss.name, {}).get(d))
|
||||
|
||||
row.append(ss.total_loan_repayment)
|
||||
row.update({frappe.scrub(d): ss_ded_map.get(ss.name, {}).get(d)})
|
||||
|
||||
if currency == company_currency:
|
||||
row += [
|
||||
flt(ss.total_deduction) * flt(ss.exchange_rate),
|
||||
flt(ss.net_pay) * flt(ss.exchange_rate),
|
||||
]
|
||||
row.update(
|
||||
{
|
||||
"gross_pay": flt(ss.gross_pay) * flt(ss.exchange_rate),
|
||||
"total_deduction": flt(ss.total_deduction) * flt(ss.exchange_rate),
|
||||
"net_pay": flt(ss.net_pay) * flt(ss.exchange_rate),
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
row += [ss.total_deduction, ss.net_pay]
|
||||
row.append(currency or company_currency)
|
||||
row.update(
|
||||
{"gross_pay": ss.gross_pay, "total_deduction": ss.total_deduction, "net_pay": ss.net_pay}
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(salary_slips):
|
||||
"""
|
||||
def get_earning_and_deduction_types(salary_slips):
|
||||
salary_component_and_type = {_("Earning"): [], _("Deduction"): []}
|
||||
|
||||
for salary_compoent in get_salary_components(salary_slips):
|
||||
component_type = get_salary_component_type(salary_compoent[0])
|
||||
salary_component_and_type[_(component_type)].append(salary_compoent[0])
|
||||
|
||||
return sorted(salary_component_and_type[_("Earning")]), sorted(
|
||||
salary_component_and_type[_("Deduction")]
|
||||
)
|
||||
|
||||
|
||||
def update_column_width(ss, columns):
|
||||
if ss.branch is not None:
|
||||
columns[3].update({"width": 120})
|
||||
if ss.department is not None:
|
||||
columns[4].update({"width": 120})
|
||||
if ss.designation is not None:
|
||||
columns[5].update({"width": 120})
|
||||
if ss.leave_without_pay is not None:
|
||||
columns[9].update({"width": 120})
|
||||
|
||||
|
||||
def get_columns(earning_types, ded_types):
|
||||
columns = [
|
||||
_("Salary Slip ID") + ":Link/Salary Slip:150",
|
||||
_("Employee") + ":Link/Employee:120",
|
||||
_("Employee Name") + "::140",
|
||||
_("Date of Joining") + "::80",
|
||||
_("Branch") + ":Link/Branch:120",
|
||||
_("Department") + ":Link/Department:120",
|
||||
_("Designation") + ":Link/Designation:120",
|
||||
_("Company") + ":Link/Company:120",
|
||||
_("Start Date") + "::80",
|
||||
_("End Date") + "::80",
|
||||
_("Leave Without Pay") + ":Float:130",
|
||||
_("Payment Days") + ":Float:120",
|
||||
_("Currency") + ":Link/Currency:80"
|
||||
]
|
||||
"""
|
||||
columns = [
|
||||
_("Salary Slip ID") + ":Link/Salary Slip:150",
|
||||
_("Employee") + ":Link/Employee:120",
|
||||
_("Employee Name") + "::140",
|
||||
_("Date of Joining") + "::80",
|
||||
_("Branch") + ":Link/Branch:-1",
|
||||
_("Department") + ":Link/Department:-1",
|
||||
_("Designation") + ":Link/Designation:120",
|
||||
_("Company") + ":Link/Company:120",
|
||||
_("Start Date") + "::80",
|
||||
_("End Date") + "::80",
|
||||
_("Leave Without Pay") + ":Float:50",
|
||||
_("Payment Days") + ":Float:120",
|
||||
{
|
||||
"label": _("Salary Slip ID"),
|
||||
"fieldname": "salary_slip_id",
|
||||
"fieldtype": "Link",
|
||||
"options": "Salary Slip",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Employee"),
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"options": "Employee",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Employee Name"),
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Date of Joining"),
|
||||
"fieldname": "data_of_joining",
|
||||
"fieldtype": "Date",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"label": _("Branch"),
|
||||
"fieldname": "branch",
|
||||
"fieldtype": "Link",
|
||||
"options": "Branch",
|
||||
"width": -1,
|
||||
},
|
||||
{
|
||||
"label": _("Department"),
|
||||
"fieldname": "department",
|
||||
"fieldtype": "Link",
|
||||
"options": "Department",
|
||||
"width": -1,
|
||||
},
|
||||
{
|
||||
"label": _("Designation"),
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"options": "Designation",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Start Date"),
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Data",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"label": _("End Date"),
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Data",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"label": _("Leave Without Pay"),
|
||||
"fieldname": "leave_without_pay",
|
||||
"fieldtype": "Float",
|
||||
"width": 50,
|
||||
},
|
||||
{
|
||||
"label": _("Payment Days"),
|
||||
"fieldname": "payment_days",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
salary_components = {_("Earning"): [], _("Deduction"): []}
|
||||
for earning in earning_types:
|
||||
columns.append(
|
||||
{
|
||||
"label": earning,
|
||||
"fieldname": frappe.scrub(earning),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
for component in frappe.db.sql(
|
||||
"""select distinct sd.salary_component, sc.type
|
||||
from `tabSalary Detail` sd, `tabSalary Component` sc
|
||||
where sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)"""
|
||||
% (", ".join(["%s"] * len(salary_slips))),
|
||||
tuple([d.name for d in salary_slips]),
|
||||
as_dict=1,
|
||||
):
|
||||
salary_components[_(component.type)].append(component.salary_component)
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Gross Pay"),
|
||||
"fieldname": "gross_pay",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns = (
|
||||
columns
|
||||
+ [(e + ":Currency:120") for e in salary_components[_("Earning")]]
|
||||
+ [_("Gross Pay") + ":Currency:120"]
|
||||
+ [(d + ":Currency:120") for d in salary_components[_("Deduction")]]
|
||||
+ [
|
||||
_("Loan Repayment") + ":Currency:120",
|
||||
_("Total Deduction") + ":Currency:120",
|
||||
_("Net Pay") + ":Currency:120",
|
||||
for deduction in ded_types:
|
||||
columns.append(
|
||||
{
|
||||
"label": deduction,
|
||||
"fieldname": frappe.scrub(deduction),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Loan Repayment"),
|
||||
"fieldname": "total_loan_repayment",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Deduction"),
|
||||
"fieldname": "total_deduction",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Net Pay"),
|
||||
"fieldname": "net_pay",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Data",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
return columns, salary_components[_("Earning")], salary_components[_("Deduction")]
|
||||
return columns
|
||||
|
||||
|
||||
def get_salary_components(salary_slips):
|
||||
return (
|
||||
frappe.qb.from_(salary_detail)
|
||||
.where((salary_detail.amount != 0) & (salary_detail.parent.isin([d.name for d in salary_slips])))
|
||||
.select(salary_detail.salary_component)
|
||||
.distinct()
|
||||
).run(as_list=True)
|
||||
|
||||
|
||||
def get_salary_component_type(salary_component):
|
||||
return frappe.db.get_value("Salary Component", salary_component, "type", cache=True)
|
||||
|
||||
|
||||
def get_salary_slips(filters, company_currency):
|
||||
filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")})
|
||||
conditions, filters = get_conditions(filters, company_currency)
|
||||
salary_slips = frappe.db.sql(
|
||||
"""select * from `tabSalary Slip` where %s
|
||||
order by employee"""
|
||||
% conditions,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2}
|
||||
|
||||
query = frappe.qb.from_(salary_slip).select(salary_slip.star)
|
||||
|
||||
if filters.get("docstatus"):
|
||||
query = query.where(salary_slip.docstatus == doc_status[filters.get("docstatus")])
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(salary_slip.start_date >= filters.get("from_date"))
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(salary_slip.end_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(salary_slip.company == filters.get("company"))
|
||||
|
||||
if filters.get("employee"):
|
||||
query = query.where(salary_slip.employee == filters.get("employee"))
|
||||
|
||||
if filters.get("currency") and filters.get("currency") != company_currency:
|
||||
query = query.where(salary_slip.currency == filters.get("currency"))
|
||||
|
||||
salary_slips = query.run(as_dict=1)
|
||||
|
||||
return salary_slips or []
|
||||
|
||||
|
||||
def get_conditions(filters, company_currency):
|
||||
conditions = ""
|
||||
doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2}
|
||||
|
||||
if filters.get("docstatus"):
|
||||
conditions += "docstatus = {0}".format(doc_status[filters.get("docstatus")])
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += " and start_date >= %(from_date)s"
|
||||
if filters.get("to_date"):
|
||||
conditions += " and end_date <= %(to_date)s"
|
||||
if filters.get("company"):
|
||||
conditions += " and company = %(company)s"
|
||||
if filters.get("employee"):
|
||||
conditions += " and employee = %(employee)s"
|
||||
if filters.get("currency") and filters.get("currency") != company_currency:
|
||||
conditions += " and currency = %(currency)s"
|
||||
|
||||
return conditions, filters
|
||||
|
||||
|
||||
def get_employee_doj_map():
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
employee,
|
||||
date_of_joining
|
||||
FROM `tabEmployee`
|
||||
"""
|
||||
employee = frappe.qb.DocType("Employee")
|
||||
|
||||
result = (frappe.qb.from_(employee).select(employee.name, employee.date_of_joining)).run()
|
||||
|
||||
return frappe._dict(result)
|
||||
|
||||
|
||||
def get_salary_slip_details(salary_slips, currency, company_currency, component_type):
|
||||
salary_slips = [ss.name for ss in salary_slips]
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(salary_slip)
|
||||
.join(salary_detail)
|
||||
.on(salary_slip.name == salary_detail.parent)
|
||||
.where((salary_detail.parent.isin(salary_slips)) & (salary_detail.parentfield == component_type))
|
||||
.select(
|
||||
salary_detail.parent,
|
||||
salary_detail.salary_component,
|
||||
salary_detail.amount,
|
||||
salary_slip.exchange_rate,
|
||||
)
|
||||
)
|
||||
).run(as_dict=1)
|
||||
|
||||
ss_map = {}
|
||||
|
||||
def get_ss_earning_map(salary_slips, currency, company_currency):
|
||||
ss_earnings = frappe.db.sql(
|
||||
"""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name
|
||||
from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)"""
|
||||
% (", ".join(["%s"] * len(salary_slips))),
|
||||
tuple([d.name for d in salary_slips]),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
ss_earning_map = {}
|
||||
for d in ss_earnings:
|
||||
ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0)
|
||||
for d in result:
|
||||
ss_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0)
|
||||
if currency == company_currency:
|
||||
ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt(
|
||||
ss_map[d.parent][d.salary_component] += flt(d.amount) * flt(
|
||||
d.exchange_rate if d.exchange_rate else 1
|
||||
)
|
||||
else:
|
||||
ss_earning_map[d.parent][d.salary_component] += flt(d.amount)
|
||||
ss_map[d.parent][d.salary_component] += flt(d.amount)
|
||||
|
||||
return ss_earning_map
|
||||
|
||||
|
||||
def get_ss_ded_map(salary_slips, currency, company_currency):
|
||||
ss_deductions = frappe.db.sql(
|
||||
"""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name
|
||||
from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)"""
|
||||
% (", ".join(["%s"] * len(salary_slips))),
|
||||
tuple([d.name for d in salary_slips]),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
ss_ded_map = {}
|
||||
for d in ss_deductions:
|
||||
ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0)
|
||||
if currency == company_currency:
|
||||
ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt(
|
||||
d.exchange_rate if d.exchange_rate else 1
|
||||
)
|
||||
else:
|
||||
ss_ded_map[d.parent][d.salary_component] += flt(d.amount)
|
||||
|
||||
return ss_ded_map
|
||||
return ss_map
|
||||
|
||||
66
erpnext/payroll/utils.py
Normal file
66
erpnext/payroll/utils.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
|
||||
def sanitize_expression(string: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Sanitizes an expression string by removing leading/trailing spaces, newlines, and line boundaries.
|
||||
|
||||
Args:
|
||||
string (Optional[str]): The input string to be sanitized (default: None).
|
||||
|
||||
Returns:
|
||||
Optional[str]: The sanitized string or None if the input string is empty or None.
|
||||
|
||||
Example:
|
||||
expression = "\r\n gross_pay > 10000\n "
|
||||
sanitized_expr = sanitize_expression(expression)
|
||||
"""
|
||||
if not string:
|
||||
return None
|
||||
|
||||
parts = string.strip().splitlines()
|
||||
string = " ".join(parts)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def prepare_error_msg(*, row: dict, error: str, expression: str, description: str) -> str:
|
||||
"""
|
||||
Prepares an error message string with formatted information about the error.
|
||||
|
||||
Args:
|
||||
row (dict): A dictionary representing the row data.
|
||||
error (str): The error message.
|
||||
expression (str): The expression that caused the error.
|
||||
description (str): Additional description or hint for the error (optional).
|
||||
|
||||
Returns:
|
||||
str: The formatted error message string.
|
||||
|
||||
Example:
|
||||
row = {
|
||||
"parenttype": "Salary Structure",
|
||||
"parent": "Salary Structure-00001",
|
||||
"parentfield": "earnings",
|
||||
"idx": 1
|
||||
}
|
||||
error = "SyntaxError: invalid syntax"
|
||||
expression = " 200 if (gross_pay>10000 and month!= 'Feb')) else 0 "
|
||||
description = "Check the syntax of the expression."
|
||||
error_msg = prepare_error_msg(row=row, error=error, expression=expression, description=description)
|
||||
"""
|
||||
msg = _("Error in {0} while evaluating the {1} {2} at row {3}").format(
|
||||
row.parentfield.title(), row.parenttype, get_link_to_form(row.parenttype, row.parent), row.idx
|
||||
)
|
||||
msg += "<br><br>{0}: {1}<br><br>{2}: {3}".format(
|
||||
frappe.bold(_("Expression:")), expression, frappe.bold(_("Error:")), error
|
||||
)
|
||||
|
||||
if description:
|
||||
msg += "<br><br>{0}: {1}".format(frappe.bold(_("Hint:")), description)
|
||||
|
||||
return msg
|
||||
@@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = {
|
||||
}
|
||||
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
gantt_custom_popup_html: function(ganttobj, task) {
|
||||
var html = `<h5><a style="text-decoration:underline"\
|
||||
href="/app/task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`;
|
||||
gantt_custom_popup_html: function (ganttobj, task) {
|
||||
let html = `
|
||||
<a class="text-white mb-2 inline-block cursor-pointer"
|
||||
href="/app/task/${ganttobj.id}"">
|
||||
${ganttobj.name}
|
||||
</a>
|
||||
`;
|
||||
|
||||
if(task.project) html += `<p>Project: ${task.project}</p>`;
|
||||
html += `<p>Progress: ${ganttobj.progress}</p>`;
|
||||
if (task.project) {
|
||||
html += `<p class="mb-1">${__("Project")}:
|
||||
<a class="text-white inline-block"
|
||||
href="/app/project/${task.project}"">
|
||||
${task.project}
|
||||
</a>
|
||||
</p>`;
|
||||
}
|
||||
html += `<p class="mb-1">
|
||||
${__("Progress")}:
|
||||
<span class="text-white">${ganttobj.progress}%</span>
|
||||
</p>`;
|
||||
|
||||
if(task._assign_list) {
|
||||
html += task._assign_list.reduce(
|
||||
(html, user) => html + frappe.avatar(user)
|
||||
, '');
|
||||
if (task._assign) {
|
||||
const assign_list = JSON.parse(task._assign);
|
||||
const assignment_wrapper = `
|
||||
<span>Assigned to:</span>
|
||||
<span class="text-white">
|
||||
${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")}
|
||||
</span>
|
||||
`;
|
||||
html += assignment_wrapper;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
return `<div class="p-3" style="min-width: 220px">${html}</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -764,11 +764,13 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
precision("base_grand_total")
|
||||
);
|
||||
}
|
||||
this.frm.doc.payments.find(pay => {
|
||||
if (pay.default) {
|
||||
pay.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
if(!this.frm.doc.is_return){
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.frm.refresh_fields();
|
||||
},
|
||||
|
||||
|
||||
@@ -173,7 +173,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
this.frm.set_query("expense_account", "items", function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company
|
||||
"company": doc.company,
|
||||
"report_type": "Profit and Loss",
|
||||
"is_group": 0
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -297,8 +299,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
});
|
||||
},
|
||||
|
||||
make_payment_request: function() {
|
||||
var me = this;
|
||||
make_payment_request() {
|
||||
let me = this;
|
||||
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
|
||||
? "Inward" : "Outward";
|
||||
|
||||
@@ -314,7 +316,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc){
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
}
|
||||
@@ -1066,6 +1068,16 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
this.frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
|
||||
apply_discount_on_item: function(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
}
|
||||
this.set_gross_profit(item);
|
||||
},
|
||||
|
||||
shipping_rule: function() {
|
||||
var me = this;
|
||||
if(this.frm.doc.shipping_rule) {
|
||||
@@ -1718,6 +1730,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
() => {
|
||||
if(args.items.length) {
|
||||
me._set_values_for_item_list(r.message.children);
|
||||
$.each(r.message.children || [], function(i, d) {
|
||||
me.apply_discount_on_item(d, d.doctype, d.name, 'discount_percentage');
|
||||
});
|
||||
}
|
||||
},
|
||||
() => { me.in_apply_price_list = false; }
|
||||
@@ -2011,7 +2026,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
},
|
||||
|
||||
prompt_user_for_reference_date(){
|
||||
var me = this;
|
||||
let me = this;
|
||||
frappe.prompt({
|
||||
label: __("Cheque/Reference Date"),
|
||||
fieldname: "reference_date",
|
||||
@@ -2038,7 +2053,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
|
||||
if(!is_eligible || !has_payment_schedule) return false;
|
||||
|
||||
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
|
||||
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount);
|
||||
return has_discount;
|
||||
},
|
||||
|
||||
|
||||
@@ -566,7 +566,6 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
fields.splice(3, 0, {
|
||||
fieldtype: 'Float',
|
||||
fieldname: "conversion_factor",
|
||||
in_list_view: 1,
|
||||
label: __("Conversion Factor"),
|
||||
precision: get_precision('conversion_factor')
|
||||
})
|
||||
@@ -574,6 +573,7 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
|
||||
new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "trans_items",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{####}",
|
||||
"creation": "2019-05-26 15:03:43.996455",
|
||||
"doctype": "DocType",
|
||||
@@ -12,7 +13,6 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fetch_from": "goal.objective",
|
||||
"fieldname": "objective",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -38,14 +38,17 @@
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-05-26 16:12:54.832058",
|
||||
"links": [],
|
||||
"modified": "2023-07-28 18:10:23.351246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Quality Management",
|
||||
"name": "Quality Goal Objective",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
"section_break_4",
|
||||
"account_number_length",
|
||||
"column_break_6",
|
||||
"temporary_against_account_number"
|
||||
"temporary_against_account_number",
|
||||
"opening_against_account_number"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -70,14 +71,23 @@
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"default": "9999",
|
||||
"description": "Will be used as against account for all normal ledger entries",
|
||||
"fieldname": "temporary_against_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Temporary Against Account Number",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "9000",
|
||||
"description": "Will be used as against account for opening ledger entries",
|
||||
"fieldname": "opening_against_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Opening Against Account Number"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-11-19 19:00:09.088816",
|
||||
"modified": "2023-06-30 00:56:59.556731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "DATEV Settings",
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DATEVSettings(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
if (
|
||||
self.temporary_against_account_number
|
||||
and len(self.temporary_against_account_number) != self.account_number_length
|
||||
):
|
||||
throw(
|
||||
_("Temporary Against Account Number must be {0} digits long").format(
|
||||
self.account_number_length
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.opening_against_account_number
|
||||
and len(self.opening_against_account_number) != self.account_number_length
|
||||
):
|
||||
throw(
|
||||
_("Opening Against Account Number must be {0} digits long").format(self.account_number_length)
|
||||
)
|
||||
|
||||
@@ -132,8 +132,12 @@ def execute(filters=None):
|
||||
"""Entry point for frappe."""
|
||||
data = []
|
||||
if filters and validate(filters):
|
||||
fn = "temporary_against_account_number"
|
||||
filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn)
|
||||
temp, opening = frappe.get_value(
|
||||
"DATEV Settings",
|
||||
filters.get("company"),
|
||||
["temporary_against_account_number", "opening_against_account_number"],
|
||||
)
|
||||
filters.update({"against_account": temp, "opening_account": opening or temp})
|
||||
data = get_transactions(filters, as_dict=0)
|
||||
|
||||
return COLUMNS, data
|
||||
@@ -315,7 +319,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
|
||||
acc.account_number as 'Konto',
|
||||
|
||||
/* against number or, if empty, party against number */
|
||||
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
|
||||
CASE gl.is_opening when 'Yes' then %(opening_account)s else %(against_account)s end as 'Gegenkonto (ohne BU-Schlüssel)',
|
||||
|
||||
'' as 'BU-Schlüssel',
|
||||
|
||||
@@ -530,18 +534,22 @@ def download_datev_csv(filters):
|
||||
filters = json.loads(filters)
|
||||
|
||||
validate(filters)
|
||||
|
||||
company = filters.get("company")
|
||||
|
||||
fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company)
|
||||
filters["fiscal_year_start"] = fiscal_year[1]
|
||||
|
||||
# set chart of accounts used
|
||||
coa = frappe.get_value("Company", company, "chart_of_accounts")
|
||||
filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
|
||||
|
||||
datev_settings = frappe.get_doc("DATEV Settings", company)
|
||||
filters["account_number_length"] = datev_settings.account_number_length
|
||||
filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number
|
||||
|
||||
filters.update(
|
||||
{
|
||||
"fiscal_year_start": fiscal_year[1],
|
||||
"skr": "04" if "SKR04" in coa else ("03" if "SKR03" in coa else ""),
|
||||
"account_number_length": datev_settings.account_number_length,
|
||||
"against_account": datev_settings.temporary_against_account_number,
|
||||
"opening_account": datev_settings.opening_against_account_number
|
||||
or datev_settings.temporary_against_account_number,
|
||||
}
|
||||
)
|
||||
|
||||
transactions = get_transactions(filters)
|
||||
account_names = get_account_names(filters)
|
||||
|
||||
@@ -139,7 +139,9 @@ def make_datev_settings(company):
|
||||
"client": company.name,
|
||||
"client_number": "12345",
|
||||
"consultant_number": "67890",
|
||||
"account_number_length": 4,
|
||||
"temporary_against_account_number": "9999",
|
||||
"opening_against_account_number": "9000",
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -152,7 +154,8 @@ class TestDatev(TestCase):
|
||||
"company": self.company.name,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"temporary_against_account_number": "9999",
|
||||
"against_account": "9999",
|
||||
"opening_account": "9000",
|
||||
}
|
||||
|
||||
make_datev_settings(self.company)
|
||||
|
||||
@@ -21,16 +21,14 @@ def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _("Employee"),
|
||||
"options": "Employee",
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"options": "Employee",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"label": _("Employee Name"),
|
||||
"options": "Employee",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Link",
|
||||
"width": 160,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 140},
|
||||
|
||||
@@ -18,16 +18,14 @@ def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _("Employee"),
|
||||
"options": "Employee",
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"options": "Employee",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"label": _("Employee Name"),
|
||||
"options": "Employee",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Link",
|
||||
"width": 160,
|
||||
},
|
||||
{"label": _("PF Account"), "fieldname": "pf_account", "fieldtype": "Data", "width": 140},
|
||||
|
||||
@@ -35,6 +35,7 @@ def add_print_formats():
|
||||
|
||||
|
||||
def add_permissions():
|
||||
frappe.reload_doc("regional", "doctype", "ksa_vat_setting", force=True)
|
||||
"""Add Permissions for KSA VAT Setting."""
|
||||
add_permission("KSA VAT Setting", "All", 0)
|
||||
for role in ("Accounts Manager", "Accounts User", "System Manager"):
|
||||
|
||||
@@ -656,11 +656,15 @@ def get_credit_limit(customer, company):
|
||||
|
||||
if not credit_limit:
|
||||
customer_group = frappe.get_cached_value("Customer", customer, "customer_group")
|
||||
credit_limit = frappe.db.get_value(
|
||||
|
||||
result = frappe.db.get_values(
|
||||
"Customer Credit Limit",
|
||||
{"parent": customer_group, "parenttype": "Customer Group", "company": company},
|
||||
"credit_limit",
|
||||
fieldname=["credit_limit", "bypass_credit_limit_check"],
|
||||
as_dict=True,
|
||||
)
|
||||
if result and not result[0].bypass_credit_limit_check:
|
||||
credit_limit = result[0].credit_limit
|
||||
|
||||
if not credit_limit:
|
||||
credit_limit = frappe.get_cached_value("Company", company, "credit_limit")
|
||||
|
||||
@@ -235,7 +235,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
}
|
||||
}
|
||||
// payment request
|
||||
if(flt(doc.per_billed)<100) {
|
||||
if(flt(doc.per_billed, precision('per_billed', doc)) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
|
||||
this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create'));
|
||||
this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create'));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user