mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-01 06:48:36 +00:00
Compare commits
222 Commits
v12.22.0
...
version-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6da1a93ae | ||
|
|
5af2b74039 | ||
|
|
1ee62a8342 | ||
|
|
74748c03e2 | ||
|
|
73484ef1bc | ||
|
|
1c25d7c6f2 | ||
|
|
432210d13d | ||
|
|
836bfc603a | ||
|
|
2d7c678a9d | ||
|
|
a92c53dd2b | ||
|
|
68fd8a1dcf | ||
|
|
619c178049 | ||
|
|
579a2366f6 | ||
|
|
6ad2684121 | ||
|
|
5b2b76c4a7 | ||
|
|
5bedb88e62 | ||
|
|
0c4a61d4e6 | ||
|
|
c8780b0062 | ||
|
|
f531d1a69b | ||
|
|
51aa55f3b0 | ||
|
|
db0e3d3fbe | ||
|
|
ccffcf2596 | ||
|
|
1413af8f3a | ||
|
|
9a1afeb13b | ||
|
|
13a9b304b7 | ||
|
|
ee7ac93ff3 | ||
|
|
3aa4411a0d | ||
|
|
7db947550b | ||
|
|
c74a6f1827 | ||
|
|
73243c8ac3 | ||
|
|
1800ce2780 | ||
|
|
e5ea16a6dd | ||
|
|
54cb62c7bf | ||
|
|
11ad77f491 | ||
|
|
95b0c743d4 | ||
|
|
fee83e8ba4 | ||
|
|
4c5b5607ba | ||
|
|
00b40c0f3c | ||
|
|
ac08379f2b | ||
|
|
753adf3ce4 | ||
|
|
fbbf29e829 | ||
|
|
ce2aa767b2 | ||
|
|
c9927efcae | ||
|
|
e9dbb46a06 | ||
|
|
39125a78e0 | ||
|
|
2a00164059 | ||
|
|
7d1953bb3b | ||
|
|
33eeb64fec | ||
|
|
bbffb5d91e | ||
|
|
260b9c1885 | ||
|
|
034e8bd028 | ||
|
|
eb8b03f590 | ||
|
|
abb4d99ca8 | ||
|
|
a6fd5a69e8 | ||
|
|
849c795113 | ||
|
|
3647a24f60 | ||
|
|
67f17c7a0c | ||
|
|
24c004b537 | ||
|
|
1fce25180c | ||
|
|
23946cea2e | ||
|
|
1b78c501a1 | ||
|
|
d09ed0a578 | ||
|
|
9b83e3856a | ||
|
|
656686f2b1 | ||
|
|
8e7ee953c1 | ||
|
|
d2af2b31e5 | ||
|
|
f79651c54d | ||
|
|
80ba0cf978 | ||
|
|
30c1a7bb20 | ||
|
|
d119b4321d | ||
|
|
4b1befb691 | ||
|
|
fa5e018330 | ||
|
|
8c84d6a1d9 | ||
|
|
f76139dceb | ||
|
|
97544e2bbc | ||
|
|
117718d410 | ||
|
|
bcb0683be1 | ||
|
|
bf5f0a226e | ||
|
|
55aaefb53c | ||
|
|
52ed3c219b | ||
|
|
1e378cfde0 | ||
|
|
29f5d434d8 | ||
|
|
aa0e21e84b | ||
|
|
34bf2004bb | ||
|
|
7ac656d737 | ||
|
|
ba537b0b93 | ||
|
|
89b0e5023a | ||
|
|
a0e4708470 | ||
|
|
e60d7ac09e | ||
|
|
d6555e8632 | ||
|
|
14602178e7 | ||
|
|
5f2819c9b1 | ||
|
|
5bc7d63646 | ||
|
|
94162e0139 | ||
|
|
e2e751e26f | ||
|
|
70244cee89 | ||
|
|
ae6e69ebd4 | ||
|
|
6fdf4dd03e | ||
|
|
4aede0ea3f | ||
|
|
de660bf9c4 | ||
|
|
391bf86e0a | ||
|
|
452c613974 | ||
|
|
dd7b02ad7d | ||
|
|
7cc02bf861 | ||
|
|
678960209c | ||
|
|
e4995dc9ed | ||
|
|
7cf5dc7dab | ||
|
|
e676a09c18 | ||
|
|
6431243ce3 | ||
|
|
49f93b347c | ||
|
|
27e9e47ba8 | ||
|
|
c9f51d3cec | ||
|
|
9ae3f26dbf | ||
|
|
ad736f1789 | ||
|
|
16ece12516 | ||
|
|
c9e75d2ab5 | ||
|
|
985fdade7e | ||
|
|
10c34da174 | ||
|
|
d1480be596 | ||
|
|
93a744dc12 | ||
|
|
a2b98bb80c | ||
|
|
4fdff12242 | ||
|
|
9c9cde48ad | ||
|
|
999a3f1305 | ||
|
|
8139672c7a | ||
|
|
7169a4c113 | ||
|
|
bc960ab35f | ||
|
|
f9e9c5f637 | ||
|
|
fbc8fb36dd | ||
|
|
6359e69503 | ||
|
|
1d68d12b7a | ||
|
|
b7a74aa578 | ||
|
|
efddcbe42e | ||
|
|
6f6e390863 | ||
|
|
7843c3d51a | ||
|
|
8871bd4bfb | ||
|
|
d0a7141e35 | ||
|
|
6fe28e83e2 | ||
|
|
abf353a286 | ||
|
|
cef6a8434a | ||
|
|
dc76094a9f | ||
|
|
1e428f9531 | ||
|
|
710c1c1786 | ||
|
|
aa358b021e | ||
|
|
8bc37da20d | ||
|
|
914709099f | ||
|
|
87dddbe868 | ||
|
|
ca42b16d3a | ||
|
|
73666982c7 | ||
|
|
48bd1965e4 | ||
|
|
ab84579b0e | ||
|
|
8064792b8c | ||
|
|
fe6c96cab9 | ||
|
|
d5e89d98c2 | ||
|
|
f8fa3860d9 | ||
|
|
d8a7abcd02 | ||
|
|
5bb9de8614 | ||
|
|
0fd50e0426 | ||
|
|
bdfc300896 | ||
|
|
14292456cf | ||
|
|
24d67c35b7 | ||
|
|
8d71fcb948 | ||
|
|
2945e604aa | ||
|
|
ea085b3a76 | ||
|
|
611966c139 | ||
|
|
d650b55f52 | ||
|
|
8eca908365 | ||
|
|
09d5ddc42b | ||
|
|
abd53b114c | ||
|
|
b1e932a6f8 | ||
|
|
e6a3e6beb7 | ||
|
|
239974c73e | ||
|
|
27ecb54b8c | ||
|
|
6b4b80a4a4 | ||
|
|
68225bbcad | ||
|
|
b549287b94 | ||
|
|
12c3e5dfd6 | ||
|
|
c908add82e | ||
|
|
c74f0f3530 | ||
|
|
2e5a358e96 | ||
|
|
01f8833bd1 | ||
|
|
5e37949411 | ||
|
|
c5a0c22352 | ||
|
|
fc0459fba9 | ||
|
|
9a1caddacc | ||
|
|
b22cbb3122 | ||
|
|
f4a9e52cbe | ||
|
|
24e1786e49 | ||
|
|
5399891b25 | ||
|
|
1b8670b263 | ||
|
|
23db6a8e3a | ||
|
|
4c58fb40a8 | ||
|
|
df045a61ed | ||
|
|
6f7410073f | ||
|
|
0ecf8f5d66 | ||
|
|
dfc68950c1 | ||
|
|
ee7de6b107 | ||
|
|
b2a090f073 | ||
|
|
704f7b57b8 | ||
|
|
8070d76450 | ||
|
|
266563a99a | ||
|
|
7270b89133 | ||
|
|
57a68c317e | ||
|
|
fc44712976 | ||
|
|
a80d9d81c8 | ||
|
|
6a30025209 | ||
|
|
b9fb2349d6 | ||
|
|
2c30ad9465 | ||
|
|
2a8b3841bf | ||
|
|
0c4f67b884 | ||
|
|
fe1d985432 | ||
|
|
57832fc9f8 | ||
|
|
371d124a0e | ||
|
|
3302ed4658 | ||
|
|
5271ce36de | ||
|
|
88bab1e3ad | ||
|
|
6ec804d77f | ||
|
|
5cced71ce4 | ||
|
|
aea8773503 | ||
|
|
a0cfe449df | ||
|
|
b135569ca6 | ||
|
|
266f24bb74 |
26
.github/workflows/backport.yml
vendored
Normal file
26
.github/workflows/backport.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
ref: develop
|
||||
- name: Install Actions
|
||||
run: npm install --production --prefix ./actions
|
||||
- name: Run backport
|
||||
uses: ./actions/backport
|
||||
with:
|
||||
token: ${{secrets.BACKPORT_BOT_TOKEN}}
|
||||
labelsToAdd: "backport"
|
||||
title: "{{originalTitle}}"
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
include:
|
||||
- name: "Python 2.7 Server Side Test"
|
||||
python: 2.7
|
||||
script: bench --site test_site run-tests --app erpnext --coverage
|
||||
script: bench --site test_site run-tests --app erpnext
|
||||
|
||||
- name: "Python 3.6 Server Side Test"
|
||||
python: 3.6
|
||||
script: bench --site test_site run-tests --app erpnext --coverage
|
||||
script: bench --site test_site run-tests --app erpnext
|
||||
|
||||
- name: "Python 2.7 Patch Test"
|
||||
python: 2.7
|
||||
@@ -74,8 +74,3 @@ install:
|
||||
- bench get-app erpnext $TRAVIS_BUILD_DIR
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
|
||||
after_script:
|
||||
- pip install coverage==4.5.4
|
||||
- pip install python-coveralls
|
||||
- coveralls -b apps/erpnext -d ../../sites/.coverage
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '12.22.0'
|
||||
__version__ = '12.31.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -109,5 +109,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
plaid_success(token, response) {
|
||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
@@ -271,6 +271,7 @@
|
||||
"label": "Debit",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
@@ -304,6 +305,7 @@
|
||||
"label": "Credit",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
@@ -632,6 +634,7 @@
|
||||
"label": "Allocated Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
@@ -731,6 +734,7 @@
|
||||
"label": "Unallocated Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
@@ -755,7 +759,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-05-11 05:27:55.244721",
|
||||
"modified": "2021-11-26 12:44:55.244721",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -7,19 +7,19 @@ DEFAULT_MAPPERS = [
|
||||
'section_header': 'Cash flows from operating activities',
|
||||
'section_leader': 'Adjustments for',
|
||||
'section_name': 'Operating Activities',
|
||||
'position': 0,
|
||||
'position': 1,
|
||||
'section_subtotal': 'Cash generated from operations',
|
||||
},
|
||||
{
|
||||
'doctype': 'Cash Flow Mapper',
|
||||
'position': 1,
|
||||
'position': 2,
|
||||
'section_footer': 'Net cash used in investing activities',
|
||||
'section_header': 'Cash flows from investing activities',
|
||||
'section_name': 'Investing Activities'
|
||||
},
|
||||
{
|
||||
'doctype': 'Cash Flow Mapper',
|
||||
'position': 2,
|
||||
'position': 3,
|
||||
'section_footer': 'Net cash used in financing activites',
|
||||
'section_header': 'Cash flows from financing activities',
|
||||
'section_name': 'Financing Activities',
|
||||
|
||||
@@ -81,10 +81,11 @@ class ExchangeRateRevaluation(Document):
|
||||
sum(debit) - sum(credit) as balance
|
||||
from `tabGL Entry`
|
||||
where account in (%s)
|
||||
group by account, party_type, party
|
||||
and posting_date <= %s
|
||||
group by account, NULLIF(party_type,''), NULLIF(party,'')
|
||||
having sum(debit) != sum(credit)
|
||||
order by account
|
||||
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
|
||||
""" % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
|
||||
|
||||
return account_details
|
||||
|
||||
@@ -124,9 +125,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"party_type": d.get("party_type"),
|
||||
"party": d.get("party"),
|
||||
"account_currency": d.get("account_currency"),
|
||||
"balance": d.get("balance_in_account_currency"),
|
||||
dr_or_cr: abs(d.get("balance_in_account_currency")),
|
||||
"exchange_rate":d.get("new_exchange_rate"),
|
||||
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
|
||||
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
})
|
||||
@@ -135,9 +136,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"party_type": d.get("party_type"),
|
||||
"party": d.get("party"),
|
||||
"account_currency": d.get("account_currency"),
|
||||
"balance": d.get("balance_in_account_currency"),
|
||||
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
|
||||
"exchange_rate": d.get("current_exchange_rate"),
|
||||
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
|
||||
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name
|
||||
})
|
||||
@@ -166,9 +167,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
|
||||
|
||||
account_details = {}
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
|
||||
balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
|
||||
if balance:
|
||||
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
|
||||
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
|
||||
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
|
||||
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
|
||||
|
||||
test_records = frappe.get_test_records('Fiscal Year')
|
||||
test_ignore = ["Company"]
|
||||
|
||||
class TestFiscalYear(unittest.TestCase):
|
||||
@@ -23,3 +25,29 @@ class TestFiscalYear(unittest.TestCase):
|
||||
})
|
||||
|
||||
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
test_records = [
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Short Fiscal Year 2011",
|
||||
"is_short_year": 1,
|
||||
"year_end_date": "2011-04-01",
|
||||
"year_start_date": "2011-12-31"
|
||||
}
|
||||
]
|
||||
|
||||
start = 2012
|
||||
end = now_datetime().year + 5
|
||||
for year in range(start, end):
|
||||
test_records.append({
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year {}".format(year),
|
||||
"year_start_date": "{}-01-01".format(year),
|
||||
"year_end_date": "{}-12-31".format(year)
|
||||
})
|
||||
|
||||
return test_records
|
||||
|
||||
test_records = test_record_generator()
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2012",
|
||||
"year_end_date": "2012-12-31",
|
||||
"year_start_date": "2012-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2013",
|
||||
"year_end_date": "2013-12-31",
|
||||
"year_start_date": "2013-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2014",
|
||||
"year_end_date": "2014-12-31",
|
||||
"year_start_date": "2014-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2015",
|
||||
"year_end_date": "2015-12-31",
|
||||
"year_start_date": "2015-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2016",
|
||||
"year_end_date": "2016-12-31",
|
||||
"year_start_date": "2016-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2017",
|
||||
"year_end_date": "2017-12-31",
|
||||
"year_start_date": "2017-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2018",
|
||||
"year_end_date": "2018-12-31",
|
||||
"year_start_date": "2018-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2019",
|
||||
"year_end_date": "2019-12-31",
|
||||
"year_start_date": "2019-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2020",
|
||||
"year_end_date": "2020-12-31",
|
||||
"year_start_date": "2020-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2021",
|
||||
"year_end_date": "2021-12-31",
|
||||
"year_start_date": "2021-01-01"
|
||||
},
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Short Fiscal Year 2021",
|
||||
"is_short_year": 1,
|
||||
"year_end_date": "2021-12-31",
|
||||
"year_start_date": "2021-04-01"
|
||||
}
|
||||
]
|
||||
@@ -97,8 +97,7 @@ class GLEntry(Document):
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
|
||||
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
|
||||
|
||||
@@ -593,12 +593,22 @@ frappe.ui.form.on('Payment Entry', {
|
||||
{fieldtype:"Column Break"},
|
||||
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
}
|
||||
}
|
||||
},
|
||||
{fieldtype:"Column Break"},
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
|
||||
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"));
|
||||
},
|
||||
@@ -1041,18 +1051,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
|
||||
frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
() => {
|
||||
if(frm.doc.payment_type != "Internal") {
|
||||
frm.clear_table("references");
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,13 +330,17 @@ def get_pricing_rule_details(args, pricing_rule):
|
||||
def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details.pricing_rule_for = pricing_rule.rate_or_discount
|
||||
|
||||
if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency)
|
||||
or (pricing_rule.margin_type == 'Percentage')):
|
||||
item_details.margin_type = pricing_rule.margin_type
|
||||
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
|
||||
else:
|
||||
item_details.margin_type = None
|
||||
item_details.margin_rate_or_amount = 0.0
|
||||
for apply_on in ['Percentage', 'Amount']:
|
||||
if pricing_rule.margin_type != apply_on:
|
||||
continue
|
||||
|
||||
field = 'margin_rate_or_amount'
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
item_details.setdefault('margin_type', apply_on)
|
||||
|
||||
item_details[field] += (pricing_rule.get(field, 0)
|
||||
if pricing_rule else args.get(field, 0))
|
||||
|
||||
if pricing_rule.rate_or_discount == 'Rate':
|
||||
pricing_rule_rate = 0.0
|
||||
|
||||
@@ -247,8 +247,15 @@ class PurchaseInvoice(BuyingController):
|
||||
else:
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
||||
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
|
||||
asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
|
||||
company = self.company)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form('Asset Category', asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account")
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif not item.expense_account and for_validate:
|
||||
|
||||
@@ -825,45 +825,43 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
frappe.ui.form.on('Sales Invoice Timesheet', {
|
||||
calculate_timesheet_totals: function(frm) {
|
||||
frm.set_value("total_billing_amount",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_amount"] || 0.0), 0.0));
|
||||
frm.set_value("total_billing_hours",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0));
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
time_sheet: function(frm, cdt, cdn){
|
||||
var d = locals[cdt][cdn];
|
||||
if(d.time_sheet) {
|
||||
frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_data",
|
||||
args: {
|
||||
'name': d.time_sheet,
|
||||
'project': frm.doc.project || null
|
||||
"name": d.time_sheet,
|
||||
"project": frm.doc.project || null
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
callback: function(r) {
|
||||
if(r.message){
|
||||
data = r.message;
|
||||
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
|
||||
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);
|
||||
calculate_total_billing_amount(frm)
|
||||
frappe.model.set_value(cdt, cdn, "billing_hours", r.message.billing_hours);
|
||||
frappe.model.set_value(cdt, cdn, "billing_amount", r.message.billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, "timesheet_detail", r.message.timesheet_detail);
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
timesheets_remove: function(frm, cdt, cdn) {
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var calculate_total_billing_amount = function(frm) {
|
||||
var doc = frm.doc;
|
||||
|
||||
doc.total_billing_amount = 0.0
|
||||
if(doc.timesheets) {
|
||||
$.each(doc.timesheets, function(index, data){
|
||||
doc.total_billing_amount += data.billing_amount
|
||||
})
|
||||
}
|
||||
|
||||
refresh_field('total_billing_amount')
|
||||
}
|
||||
|
||||
var select_loyalty_program = function(frm, loyalty_programs) {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"time_sheet_list",
|
||||
"timesheets",
|
||||
"total_billing_amount",
|
||||
"total_billing_hours",
|
||||
"section_break_30",
|
||||
"total_qty",
|
||||
"base_total",
|
||||
@@ -1564,12 +1565,20 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_billing_hours",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Billing Hours",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-07-01 12:41:29.484813",
|
||||
"modified": "2021-07-26 14:01:34.605644",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -683,12 +683,11 @@ class SalesInvoice(SellingController):
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
def calculate_billing_amount_for_timesheet(self):
|
||||
total_billing_amount = 0.0
|
||||
for data in self.timesheets:
|
||||
if data.billing_amount:
|
||||
total_billing_amount += data.billing_amount
|
||||
def timesheet_sum(field):
|
||||
return sum((ts.get(field) or 0.0) for ts in self.timesheets)
|
||||
|
||||
self.total_billing_amount = total_billing_amount
|
||||
self.total_billing_amount = timesheet_sum("billing_amount")
|
||||
self.total_billing_hours = timesheet_sum("billing_hours")
|
||||
|
||||
def get_warehouse(self):
|
||||
user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"additional_discount_percentage",
|
||||
"additional_discount_amount",
|
||||
"sb_3",
|
||||
"submit_invoice",
|
||||
"invoices",
|
||||
"accounting_dimensions_section",
|
||||
"dimension_col_break"
|
||||
@@ -202,9 +203,15 @@
|
||||
"fieldname": "generate_new_invoices_past_due_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Generate New Invoices Past Due Date"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "submit_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit Invoice Automatically"
|
||||
}
|
||||
],
|
||||
"modified": "2020-11-29 22:46:14.879289",
|
||||
"modified": "2021-05-03 13:35:21.422940",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
||||
@@ -288,8 +288,11 @@ class Subscription(Document):
|
||||
invoice.to_date = self.current_invoice_end
|
||||
|
||||
invoice.flags.ignore_mandatory = True
|
||||
invoice.set_missing_values()
|
||||
invoice.save()
|
||||
invoice.submit()
|
||||
|
||||
if self.submit_invoice:
|
||||
invoice.submit()
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.submit()
|
||||
|
||||
def validate_account_for_perpetual_inventory(gl_map):
|
||||
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
|
||||
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)) and gl_map[0].voucher_type=="Journal Entry":
|
||||
account_list = [gl_entries.account for gl_entries in gl_map]
|
||||
|
||||
aii_accounts = [d.name for d in frappe.get_all("Account",
|
||||
@@ -160,13 +160,12 @@ def validate_account_for_perpetual_inventory(gl_map):
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
|
||||
gl_map[0].posting_date, gl_map[0].company)
|
||||
|
||||
if gl_map[0].voucher_type=="Journal Entry":
|
||||
# In case of Journal Entry, there are no corresponding SL entries,
|
||||
# hence deducting currency amount
|
||||
account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
|
||||
if account_bal == stock_bal:
|
||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||
.format(account), StockAccountInvalidTransaction)
|
||||
# In case of Journal Entry, there are no corresponding SL entries,
|
||||
# hence deducting currency amount
|
||||
account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
|
||||
if account_bal == stock_bal:
|
||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||
.format(account), StockAccountInvalidTransaction)
|
||||
|
||||
# This has been comment for a temporary, will add this code again on release of immutable ledger
|
||||
# elif account_bal != stock_bal:
|
||||
@@ -294,7 +293,8 @@ def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
|
||||
select account, posting_date, party_type, party, cost_center, fiscal_year,voucher_type,
|
||||
voucher_no, against_voucher_type, against_voucher, cost_center, company
|
||||
from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no), as_dict=True)
|
||||
where voucher_type=%s and voucher_no=%s
|
||||
for update""", (voucher_type, voucher_no), as_dict=True)
|
||||
|
||||
if gl_entries:
|
||||
validate_accounting_period(gl_entries)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||
|
||||
<div class="page-break">
|
||||
{% if doc.signed_einvoice %}
|
||||
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
@@ -163,4 +164,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center" style="color: #98A1A9; font-size: 14px;">
|
||||
You must generate IRN before you can preview GST E-Invoice.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ class TestAccountBalance(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
|
||||
self.assertEqual(expected_data, report[1])
|
||||
expected_data = sorted(expected_data, key=lambda k:k['account'])
|
||||
output = sorted(report[1], key=lambda k:k['account'])
|
||||
|
||||
self.assertEqual(expected_data, output)
|
||||
|
||||
def make_sales_invoice():
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -36,5 +36,20 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
|
||||
"default": "Invoice"
|
||||
},
|
||||
]
|
||||
],
|
||||
"tree": true,
|
||||
"name_field": "parent",
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && data.indent == 0.0) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-08-13 11:26:39.112352",
|
||||
"modified": "2021-08-19 18:57:07.468202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Gross Profit",
|
||||
"report_type": "Script Report",
|
||||
|
||||
@@ -41,16 +41,44 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(group_wise_columns, filters)
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
if filters.group_by == 'Invoice':
|
||||
get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data)
|
||||
|
||||
else:
|
||||
get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data):
|
||||
column_names = get_column_names()
|
||||
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = 'Sales Invoice:Link/Item:300'
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
|
||||
for src in gross_profit_data.si_list:
|
||||
row = frappe._dict()
|
||||
row.indent = src.indent
|
||||
row.parent_invoice = src.parent_invoice
|
||||
row.currency = filters.currency
|
||||
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row[column_names[col]] = src.get(col)
|
||||
|
||||
data.append(row)
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for idx, src in enumerate(gross_profit_data.grouped_data):
|
||||
row = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
|
||||
row.append(filters.currency)
|
||||
if idx == len(gross_profit_data.grouped_data)-1:
|
||||
row[0] = frappe.bold("Total")
|
||||
data.append(row)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
column_map = frappe._dict({
|
||||
@@ -91,12 +119,38 @@ def get_columns(group_wise_columns, filters):
|
||||
|
||||
return columns
|
||||
|
||||
def get_column_names():
|
||||
return frappe._dict({
|
||||
'parent': 'sales_invoice',
|
||||
'customer': 'customer',
|
||||
'customer_group': 'customer_group',
|
||||
'posting_date': 'posting_date',
|
||||
'item_code': 'item_code',
|
||||
'item_name': 'item_name',
|
||||
'item_group': 'item_group',
|
||||
'brand': 'brand',
|
||||
'description': 'description',
|
||||
'warehouse': 'warehouse',
|
||||
'qty': 'qty',
|
||||
'base_rate': 'avg._selling_rate',
|
||||
'buying_rate': 'valuation_rate',
|
||||
'base_amount': 'selling_amount',
|
||||
'buying_amount': 'buying_amount',
|
||||
'gross_profit': 'gross_profit',
|
||||
'gross_profit_percent': 'gross_profit_%',
|
||||
'project': 'project'
|
||||
})
|
||||
|
||||
class GrossProfitGenerator(object):
|
||||
def __init__(self, filters=None):
|
||||
self.data = []
|
||||
self.average_buying_rate = {}
|
||||
self.filters = frappe._dict(filters)
|
||||
self.load_invoice_items()
|
||||
|
||||
if filters.group_by == 'Invoice':
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_stock_ledger_entries()
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
@@ -110,7 +164,12 @@ class GrossProfitGenerator(object):
|
||||
self.currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
self.float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
for row in self.si_list:
|
||||
grouped_by_invoice = True if self.filters.get("group_by") == "Invoice" else False
|
||||
|
||||
if grouped_by_invoice:
|
||||
buying_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.skip_row(row, self.product_bundles):
|
||||
continue
|
||||
|
||||
@@ -132,12 +191,20 @@ class GrossProfitGenerator(object):
|
||||
row.buying_amount = flt(self.get_buying_amount(row, row.item_code),
|
||||
self.currency_precision)
|
||||
|
||||
if grouped_by_invoice:
|
||||
if row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
elif row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
buying_amount = 0
|
||||
|
||||
# get buying rate
|
||||
if row.qty:
|
||||
row.buying_rate = flt(row.buying_amount / row.qty, self.float_precision)
|
||||
row.base_rate = flt(row.base_amount / row.qty, self.float_precision)
|
||||
else:
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
if self.is_not_invoice_row(row):
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
@@ -154,6 +221,15 @@ class GrossProfitGenerator(object):
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
# sum buying / selling totals for group
|
||||
self.totals = frappe._dict(
|
||||
qty=0,
|
||||
base_amount=0,
|
||||
buying_amount=0,
|
||||
gross_profit=0,
|
||||
gross_profit_percent=0,
|
||||
base_rate=0,
|
||||
buying_rate=0
|
||||
)
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
@@ -165,6 +241,7 @@ class GrossProfitGenerator(object):
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
self.add_to_totals(new_row)
|
||||
else:
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if row.parent in self.returned_invoices \
|
||||
@@ -173,19 +250,32 @@ class GrossProfitGenerator(object):
|
||||
for returned_item_row in returned_item_rows:
|
||||
row.qty += returned_item_row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(row.qty * row.buying_rate, self.currency_precision)
|
||||
if row.qty or row.base_amount:
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row):
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
self.add_to_totals(row)
|
||||
self.set_average_gross_profit(self.totals)
|
||||
self.grouped_data.append(self.totals)
|
||||
|
||||
def is_not_invoice_row(self, row):
|
||||
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
|
||||
|
||||
def set_average_rate(self, new_row):
|
||||
self.set_average_gross_profit(new_row)
|
||||
new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
|
||||
new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
|
||||
return new_row
|
||||
|
||||
def set_average_gross_profit(self, new_row):
|
||||
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
|
||||
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
|
||||
if new_row.base_amount else 0
|
||||
new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
|
||||
new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
|
||||
|
||||
return new_row
|
||||
def add_to_totals(self, new_row):
|
||||
for key in self.totals:
|
||||
if new_row.get(key):
|
||||
self.totals[key] += new_row[key]
|
||||
|
||||
def get_returned_invoice_items(self):
|
||||
returned_invoices = frappe.db.sql("""
|
||||
@@ -334,6 +424,109 @@ class GrossProfitGenerator(object):
|
||||
.format(conditions=conditions, sales_person_cols=sales_person_cols,
|
||||
sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1)
|
||||
|
||||
def group_items_by_invoice(self):
|
||||
"""
|
||||
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
|
||||
"""
|
||||
|
||||
parents = []
|
||||
|
||||
for row in self.si_list:
|
||||
if row.parent not in parents:
|
||||
parents.append(row.parent)
|
||||
|
||||
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
|
||||
|
||||
else:
|
||||
# skipping the bundle items rows
|
||||
if not row.indent:
|
||||
row.indent = 1.0
|
||||
row.parent_invoice = row.parent
|
||||
row.parent = row.item_code
|
||||
|
||||
if frappe.db.exists('Product Bundle', row.item_code):
|
||||
self.add_bundle_items(row, index)
|
||||
|
||||
def get_invoice_row(self, row):
|
||||
return frappe._dict({
|
||||
'parent_invoice': "",
|
||||
'indent': 0.0,
|
||||
'parent': row.parent,
|
||||
'posting_date': row.posting_date,
|
||||
'posting_time': row.posting_time,
|
||||
'project': row.project,
|
||||
'update_stock': row.update_stock,
|
||||
'customer': row.customer,
|
||||
'customer_group': row.customer_group,
|
||||
'item_code': None,
|
||||
'item_name': None,
|
||||
'description': None,
|
||||
'warehouse': None,
|
||||
'item_group': None,
|
||||
'brand': None,
|
||||
'dn_detail': None,
|
||||
'delivery_note': None,
|
||||
'qty': None,
|
||||
'item_row': None,
|
||||
'is_return': row.is_return,
|
||||
'cost_center': row.cost_center,
|
||||
'base_net_amount': frappe.db.get_value('Sales Invoice', row.parent, 'base_net_total')
|
||||
})
|
||||
|
||||
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']
|
||||
)
|
||||
|
||||
def get_bundle_item_row(self, product_bundle, item):
|
||||
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
|
||||
|
||||
return frappe._dict({
|
||||
'parent_invoice': product_bundle.item_code,
|
||||
'indent': product_bundle.indent + 1,
|
||||
'parent': item.item_code,
|
||||
'posting_date': product_bundle.posting_date,
|
||||
'posting_time': product_bundle.posting_time,
|
||||
'project': product_bundle.project,
|
||||
'customer': product_bundle.customer,
|
||||
'customer_group': product_bundle.customer_group,
|
||||
'item_code': item.item_code,
|
||||
'item_name': item_name,
|
||||
'description': description,
|
||||
'warehouse': product_bundle.warehouse,
|
||||
'item_group': item_group,
|
||||
'brand': brand,
|
||||
'dn_detail': product_bundle.dn_detail,
|
||||
'delivery_note': product_bundle.delivery_note,
|
||||
'qty': (flt(product_bundle.qty) * flt(item.qty)),
|
||||
'item_row': None,
|
||||
'is_return': product_bundle.is_return,
|
||||
'cost_center': product_bundle.cost_center
|
||||
})
|
||||
|
||||
def get_bundle_item_details(self, item_code):
|
||||
return frappe.db.get_value(
|
||||
'Item',
|
||||
item_code,
|
||||
['item_name', 'description', 'item_group', 'brand']
|
||||
)
|
||||
|
||||
def load_stock_ledger_entries(self):
|
||||
res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
|
||||
voucher_detail_no, stock_value, warehouse, actual_qty as qty
|
||||
|
||||
@@ -78,6 +78,7 @@ frappe.ui.form.on('Asset', {
|
||||
frappe.ui.form.trigger("Asset", "is_existing_asset");
|
||||
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
||||
frm.events.make_schedules_editable(frm);
|
||||
frm.trigger("toggle_make_depreciation_entry");
|
||||
|
||||
if (frm.doc.docstatus==1) {
|
||||
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
|
||||
@@ -141,6 +142,18 @@ frappe.ui.form.on('Asset', {
|
||||
}
|
||||
},
|
||||
|
||||
toggle_make_depreciation_entry: function(frm) {
|
||||
if (frm.doc.calculate_depreciation){
|
||||
if (in_list(["Submitted", "Partially Depreciated"], frm.doc.status)){
|
||||
frm.fields_dict['schedules'].grid.set_column_disp('make_depreciation_entry', true);
|
||||
} else {
|
||||
frm.fields_dict['schedules'].grid.set_column_disp('make_depreciation_entry', false);
|
||||
}
|
||||
|
||||
frm.refresh_field('schedules');
|
||||
}
|
||||
},
|
||||
|
||||
toggle_reference_doc: function(frm) {
|
||||
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
|
||||
frm.set_df_property('purchase_invoice', 'read_only', 1);
|
||||
|
||||
@@ -122,11 +122,6 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
|
||||
docname = self.purchase_receipt or self.purchase_invoice
|
||||
if docname:
|
||||
doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
|
||||
date = frappe.db.get_value(doctype, docname, 'posting_date')
|
||||
|
||||
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
@@ -404,9 +399,10 @@ class Asset(AccountsController):
|
||||
if accumulated_depreciation_after_full_schedule:
|
||||
accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule)
|
||||
|
||||
asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
|
||||
flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision('gross_purchase_amount'))
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation) -
|
||||
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
||||
|
||||
if (row.expected_value_after_useful_life and
|
||||
row.expected_value_after_useful_life < asset_value_after_full_schedule):
|
||||
|
||||
@@ -34,6 +34,8 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
date = today()
|
||||
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
validate_asset(asset)
|
||||
|
||||
fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account = \
|
||||
get_depreciation_accounts(asset)
|
||||
|
||||
@@ -59,7 +61,7 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
"credit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": ""
|
||||
"cost_center": depreciation_cost_center
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
@@ -101,6 +103,10 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
|
||||
return asset
|
||||
|
||||
def validate_asset(asset):
|
||||
if asset.status not in ['Submitted', 'Partially Depreciated']:
|
||||
frappe.throw(_("Cannot depreciate {0} Asset").format(asset.status))
|
||||
|
||||
def get_depreciation_accounts(asset):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
material_request_type: "Purchase",
|
||||
docstatus: 1,
|
||||
status: ["!=", "Stopped"],
|
||||
per_ordered: ["<", 99.99],
|
||||
per_ordered: ["<", 100],
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
|
||||
@@ -271,7 +271,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
material_request_type: "Purchase",
|
||||
docstatus: 1,
|
||||
status: ["!=", "Stopped"],
|
||||
per_ordered: ["<", 99.99]
|
||||
per_ordered: ["<", 100]
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
@@ -316,7 +316,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
material_request_type: "Purchase",
|
||||
docstatus: 1,
|
||||
status: ["!=", "Stopped"],
|
||||
per_ordered: ["<", 99.99]
|
||||
per_ordered: ["<", 100]
|
||||
}
|
||||
});
|
||||
$(btn).done_working();
|
||||
|
||||
@@ -279,19 +279,21 @@ def add_items(sq_doc, supplier, items):
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
sq_doc.append('items', {
|
||||
"item_code": data.item_code,
|
||||
"item_name": data.item_name,
|
||||
"description": data.description,
|
||||
"qty": data.qty,
|
||||
"rate": data.rate,
|
||||
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
|
||||
"warehouse": data.warehouse or '',
|
||||
args = {}
|
||||
|
||||
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
|
||||
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update({
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier",
|
||||
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
|
||||
})
|
||||
|
||||
sq_doc.append('items', args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier_idx):
|
||||
doc = get_rfq_doc(doctype, name, supplier_idx)
|
||||
|
||||
@@ -46,7 +46,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
|
||||
material_request_type: "Purchase",
|
||||
docstatus: 1,
|
||||
status: ["!=", "Stopped"],
|
||||
per_ordered: ["<", 99.99]
|
||||
per_ordered: ["<", 100]
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
|
||||
20
erpnext/change_log/v12/v12_23_0.md
Normal file
20
erpnext/change_log/v12/v12_23_0.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Version 12.23.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Added Permissions for employee to book an appointment ([#26246](https://github.com/frappe/erpnext/pull/26246))
|
||||
- New check field in subscriptions for (not) submitting invoices (BP #25394) ([#25560](https://github.com/frappe/erpnext/pull/25560))
|
||||
- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26406](https://github.com/frappe/erpnext/pull/26406))
|
||||
- Omit item discount amount for e-invoicing (#26353) ([#26408](https://github.com/frappe/erpnext/pull/26408))
|
||||
- fix(plaid): cannot reset plaid link for a bank account ([#26282](https://github.com/frappe/erpnext/pull/26282))
|
||||
- Job applicant link issue ([#25935](https://github.com/frappe/erpnext/pull/25935))
|
||||
- LMS progress issue ([#26254](https://github.com/frappe/erpnext/pull/26254))
|
||||
- Half day to be accounted in its leave type ([#26267](https://github.com/frappe/erpnext/pull/26267))
|
||||
- Material request status issue ([#26089](https://github.com/frappe/erpnext/pull/26089))
|
||||
- fix(e-invoicing): service item check ([#26141](https://github.com/frappe/erpnext/pull/26141))
|
||||
- Invoices can alter profit and loss of a closed year ([#26161](https://github.com/frappe/erpnext/pull/26161))
|
||||
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26117](https://github.com/frappe/erpnext/pull/26117))
|
||||
- Update positions in default cashflow mappers ([#26091](https://github.com/frappe/erpnext/pull/26091))
|
||||
- Staffing plan vacancies data type issue ([#25940](https://github.com/frappe/erpnext/pull/25940))
|
||||
- Added company filter while fetching loans ([#26296](https://github.com/frappe/erpnext/pull/26296))
|
||||
- Serial no issue in subcontract purchase receipt ([#26423](https://github.com/frappe/erpnext/pull/26423))
|
||||
- Fixed rounding off ordered percent to 100 in condition ([#26153](https://github.com/frappe/erpnext/pull/26153))
|
||||
13
erpnext/change_log/v12/v12_24_0.md
Normal file
13
erpnext/change_log/v12/v12_24_0.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Version 12.24.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Sales order qty update fails in "Update Items" button ([#26992](https://github.com/frappe/erpnext/pull/26992))
|
||||
- Speed up validate_account_for_perpetual_inventory ([#26730](https://github.com/frappe/erpnext/pull/26730))
|
||||
- Deadlock while doing payment reconciliation ([#26674](https://github.com/frappe/erpnext/pull/26674))
|
||||
- Remove incorrect condition in GLE comparison ([#26713](https://github.com/frappe/erpnext/pull/26713))
|
||||
- Cannot cancel invoice if IRN cancelled on portal ([#26880](https://github.com/frappe/erpnext/pull/26880))
|
||||
- Item name is missing into job card ([#26956](https://github.com/frappe/erpnext/pull/26956))
|
||||
- Removed company filter for Loan Type ([#26463](https://github.com/frappe/erpnext/pull/26463))
|
||||
- Exchange rate revaluation posting date and precision fixes ([#26629](https://github.com/frappe/erpnext/pull/26629))
|
||||
- Fixed clearing issue of payment references on setting cost center ([#26548](https://github.com/frappe/erpnext/pull/26548))
|
||||
- Increase number of supported currency exchanges ([#25722](https://github.com/frappe/erpnext/pull/25722))
|
||||
10
erpnext/change_log/v12/v12_25_0.md
Normal file
10
erpnext/change_log/v12/v12_25_0.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## Version 12.25.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Multiple price rules margin. ([#24844](https://github.com/frappe/erpnext/pull/24844))
|
||||
- Document naming rule not working for subscription invoices ([#27394](https://github.com/frappe/erpnext/pull/27394))
|
||||
- Prematurely referenced variable in buying controller for subcontracting ([#27333](https://github.com/frappe/erpnext/pull/27333))
|
||||
- Calculation of gross profit percentage in Gross Profit Report ([#26713](https://github.com/frappe/erpnext/pull/27045))
|
||||
- Price list rate not fetched for return sales invoice fixed ([#26593](https://github.com/frappe/erpnext/pull/26593))
|
||||
- Set production plan to completed even on over production ([#27027](https://github.com/frappe/erpnext/pull/27027))
|
||||
- Add `total_billing_hours` to Sales Invoice ([#26652](https://github.com/frappe/erpnext/pull/26652))
|
||||
8
erpnext/change_log/v12/v12_26_0.md
Normal file
8
erpnext/change_log/v12/v12_26_0.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## ERPNext Version 12.26.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Make Gross Profit Report more readable ([#27124](https://github.com/frappe/erpnext/pull/27124))
|
||||
- Set item uom as stock_uom if it isn't set ([#27623](https://github.com/frappe/erpnext/pull/27623))
|
||||
- Adding empty row on new maintenance visit ([#27626](https://github.com/frappe/erpnext/pull/27626))
|
||||
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#28017](https://github.com/frappe/erpnext/pull/28017))
|
||||
- Validate if item exists on uploading items in stock reco ([#27538](https://github.com/frappe/erpnext/pull/27538))
|
||||
9
erpnext/change_log/v12/v12_27_0.md
Normal file
9
erpnext/change_log/v12/v12_27_0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## ERPNext Version 12.27.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Always expect signature in webhook requests for WooCommerce ([#28367](https://github.com/frappe/erpnext/pull/28367))
|
||||
- Debit & Credit currency in bank transaction ([#28574](https://github.com/frappe/erpnext/pull/28574))
|
||||
- Incorrect balance in "Warehouse Wise Item Balance and Age" report ([#28583](https://github.com/frappe/erpnext/pull/28583))
|
||||
- Check if gst_category exists while validating GSTIN ([#28065](https://github.com/frappe/erpnext/pull/28065))
|
||||
- Skip empty rows while updating unsaved BOM cost ([#28136](https://github.com/frappe/erpnext/pull/28136))
|
||||
- Remove warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195))
|
||||
7
erpnext/change_log/v12/v12_28_0.md
Normal file
7
erpnext/change_log/v12/v12_28_0.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## ERPNext Version 12.28.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
- Set cost center for credit entries while posting Depreciation Entries ([#28908](https://github.com/frappe/erpnext/pull/28908))
|
||||
- Incorrect amount based on Payment Days in timesheet based salary slip ([#28884](https://github.com/frappe/erpnext/pull/28884))
|
||||
- Removed rename feature for the Warehouse document ([#28712](https://github.com/frappe/erpnext/pull/28712))
|
||||
- Actual tax conversion in case of multicurrency invoices ([#28539](https://github.com/frappe/erpnext/pull/28539))
|
||||
5
erpnext/change_log/v12/v12_29_0.md
Normal file
5
erpnext/change_log/v12/v12_29_0.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## ERPNext Version 12.29.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
|
||||
- Display 'Make Depreciation Entry' only for submitted or partially depreciated Assets ([#29291](https://github.com/frappe/erpnext/pull/29291))
|
||||
7
erpnext/change_log/v12/v12_30_0.md
Normal file
7
erpnext/change_log/v12/v12_30_0.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## ERPNext Version 12.29.0 Release Notes
|
||||
|
||||
### Fixes & Enhancements
|
||||
|
||||
* fix(e-invoicing): remove batch no from e-invoices ([#30088](https://github.com/frappe/erpnext/pull/30088))
|
||||
* fix: salary slip amount rounding errors ([#30250](https://github.com/frappe/erpnext/pull/30250))
|
||||
* feat: add client id & client secret fields in e invoice settings ([#30401](https://github.com/frappe/erpnext/pull/30401))
|
||||
@@ -1290,6 +1290,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
|
||||
if not d.get("item_code"):
|
||||
# ignore empty rows
|
||||
continue
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
check_doc_permissions(parent, 'create')
|
||||
@@ -1312,7 +1317,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc
|
||||
date_unchanged = prev_date == getdate(new_date) if prev_date and new_date else False # in case of delivery note etc
|
||||
if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged:
|
||||
continue
|
||||
|
||||
|
||||
@@ -981,8 +981,16 @@ def get_non_stock_items(purchase_order, fg_item_code):
|
||||
|
||||
|
||||
def set_serial_nos(raw_material, consumed_serial_nos, qty):
|
||||
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
|
||||
set(get_serial_nos(consumed_serial_nos))
|
||||
consumed_serial_nos_list = []
|
||||
|
||||
if consumed_serial_nos and isinstance(consumed_serial_nos, list):
|
||||
for row in consumed_serial_nos:
|
||||
consumed_serial_nos_list.extend(get_serial_nos(row))
|
||||
elif consumed_serial_nos:
|
||||
consumed_serial_nos_list = get_serial_nos(consumed_serial_nos)
|
||||
|
||||
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - set(consumed_serial_nos_list)
|
||||
|
||||
if serial_nos and qty <= len(serial_nos):
|
||||
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import flt, comma_or, nowdate, getdate
|
||||
from frappe.utils import flt, comma_or, nowdate, getdate, now
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
@@ -307,10 +307,14 @@ class StatusUpdater(Document):
|
||||
target.notify_update()
|
||||
|
||||
def _update_modified(self, args, update_modified):
|
||||
args['update_modified'] = ''
|
||||
if update_modified:
|
||||
args['update_modified'] = ', modified = now(), modified_by = {0}'\
|
||||
.format(frappe.db.escape(frappe.session.user))
|
||||
if not update_modified:
|
||||
args['update_modified'] = ''
|
||||
return
|
||||
|
||||
args['update_modified'] = ', modified = {0}, modified_by = {1}'.format(
|
||||
frappe.db.escape(now()),
|
||||
frappe.db.escape(frappe.session.user)
|
||||
)
|
||||
|
||||
def update_billing_status_for_zero_amount_refdoc(self, ref_dt):
|
||||
ref_fieldname = frappe.scrub(ref_dt)
|
||||
|
||||
@@ -418,7 +418,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle):
|
||||
for e in existing_gle:
|
||||
if entry.account == e.account:
|
||||
account_existed = True
|
||||
if entry.account == e.account and entry.against_account == e.against_account \
|
||||
if entry.account == e.account \
|
||||
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
|
||||
and (entry.debit != e.debit or entry.credit != e.credit):
|
||||
matched = False
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-14 15:23:54.630731",
|
||||
"modified": "2021-06-28 16:27:53.235714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
@@ -144,6 +144,18 @@
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Employee",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -345,11 +345,11 @@ def get_or_create_course_enrollment(course, program):
|
||||
student = get_current_student()
|
||||
course_enrollment = get_enrollment("course", course, student.name)
|
||||
if not course_enrollment:
|
||||
program_enrollment = get_enrollment('program', program, student.name)
|
||||
program_enrollment = get_enrollment('program', program.name, student.name)
|
||||
if not program_enrollment:
|
||||
frappe.throw(_("You are not enrolled in program {0}".format(program)))
|
||||
return
|
||||
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
|
||||
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
|
||||
else:
|
||||
return frappe.get_doc('Course Enrollment', course_enrollment)
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ def verify_request():
|
||||
)
|
||||
|
||||
if frappe.request.data and \
|
||||
frappe.get_request_header("X-Wc-Webhook-Signature") and \
|
||||
not sig == bytes(frappe.get_request_header("X-Wc-Webhook-Signature").encode()):
|
||||
not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode():
|
||||
frappe.throw(_("Unverified Webhook Data"))
|
||||
frappe.set_user(woocommerce_settings.creation_user)
|
||||
|
||||
|
||||
@@ -99,5 +99,7 @@ class PlaidConnector():
|
||||
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
|
||||
transactions.extend(response["transactions"])
|
||||
return transactions
|
||||
except ItemError as e:
|
||||
raise e
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
|
||||
|
||||
@@ -15,6 +15,10 @@ frappe.ui.form.on('Plaid Settings', {
|
||||
frm.add_custom_button(__('Link a new bank account'), () => {
|
||||
new erpnext.integrations.plaidLink(frm);
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Reset Plaid Link'), () => {
|
||||
new erpnext.integrations.plaidLink(frm);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_months, formatdate, getdate, today
|
||||
|
||||
from plaid.errors import ItemError
|
||||
|
||||
class PlaidSettings(Document):
|
||||
@staticmethod
|
||||
@@ -50,7 +51,7 @@ def add_institution(token, response):
|
||||
})
|
||||
bank.insert()
|
||||
except Exception:
|
||||
frappe.throw(frappe.get_traceback())
|
||||
frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error'))
|
||||
else:
|
||||
bank = frappe.get_doc("Bank", response["institution"]["name"])
|
||||
bank.plaid_access_token = access_token
|
||||
@@ -82,7 +83,10 @@ def add_bank_accounts(response, bank, company):
|
||||
if not acc_subtype:
|
||||
add_account_subtype(account["subtype"])
|
||||
|
||||
if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])):
|
||||
bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
|
||||
existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
|
||||
|
||||
if not existing_bank_account:
|
||||
try:
|
||||
new_account = frappe.get_doc({
|
||||
"doctype": "Bank Account",
|
||||
@@ -102,10 +106,27 @@ def add_bank_accounts(response, bank, company):
|
||||
except frappe.UniqueValidationError:
|
||||
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
|
||||
except Exception:
|
||||
frappe.throw(frappe.get_traceback())
|
||||
frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
|
||||
frappe.throw(_("There was an error creating Bank Account while linking with Plaid."),
|
||||
title=_("Plaid Link Failed"))
|
||||
|
||||
else:
|
||||
result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name"))
|
||||
try:
|
||||
existing_account = frappe.get_doc('Bank Account', existing_bank_account)
|
||||
existing_account.update({
|
||||
"bank": bank["bank_name"],
|
||||
"account_name": account["name"],
|
||||
"account_type": account.get("type", ""),
|
||||
"account_subtype": account.get("subtype", ""),
|
||||
"mask": account.get("mask", ""),
|
||||
"integration_id": account["id"]
|
||||
})
|
||||
existing_account.save()
|
||||
result.append(existing_bank_account)
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
|
||||
frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format(
|
||||
existing_bank_account), title=_("Plaid Link Failed"))
|
||||
|
||||
return result
|
||||
|
||||
@@ -174,7 +195,15 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
||||
account_id = None
|
||||
|
||||
plaid = PlaidConnector(access_token)
|
||||
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
||||
|
||||
transactions = []
|
||||
try:
|
||||
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
||||
except ItemError as e:
|
||||
if e.code == "ITEM_LOGIN_REQUIRED":
|
||||
msg = _("There was an error syncing transactions.") + " "
|
||||
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
||||
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
||||
|
||||
return transactions
|
||||
|
||||
|
||||
@@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', {
|
||||
})
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
|
||||
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
|
||||
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,5 +64,8 @@ def dump_request_data(data, event="create/order"):
|
||||
@frappe.whitelist()
|
||||
def resync(method, name, request_data):
|
||||
frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False)
|
||||
if not method.startswith("erpnext.erpnext_integrations.connectors.shopify_connection"):
|
||||
return
|
||||
|
||||
frappe.enqueue(method=method, queue='short', timeout=300, is_async=True,
|
||||
**{"order": json.loads(request_data), "request_id": name})
|
||||
|
||||
@@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
|
||||
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
|
||||
|
||||
}
|
||||
|
||||
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
|
||||
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
|
||||
|
||||
})
|
||||
|
||||
$.extend(erpnext_integrations.shopify_settings, {
|
||||
|
||||
@@ -18,7 +18,6 @@ def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
||||
)
|
||||
|
||||
if frappe.request.data and \
|
||||
frappe.get_request_header(hmac_key) and \
|
||||
not sig == bytes(frappe.get_request_header(hmac_key).encode()):
|
||||
frappe.throw(_("Unverified Webhook Data"))
|
||||
frappe.set_user(settings.modified_by)
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status==\"On Leave\"",
|
||||
"depends_on": "eval:doc.status==\"On Leave\" || doc.status==\"Half Day\"",
|
||||
"fieldname": "leave_type",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
@@ -174,7 +174,7 @@
|
||||
"icon": "fa fa-ok",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-02-19 14:25:32.945842",
|
||||
"modified": "2021-06-30 14:42:39.162146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Attendance",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.listview_settings['Job Applicant'] = {
|
||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Accepted") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
frappe.ui.form.on('Loan Application', {
|
||||
refresh: function(frm) {
|
||||
frm.trigger("toggle_fields")
|
||||
frm.trigger("add_toolbar_buttons")
|
||||
frm.trigger("toggle_fields");
|
||||
frm.trigger("add_toolbar_buttons");
|
||||
},
|
||||
repayment_method: function(frm) {
|
||||
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
|
||||
|
||||
@@ -5,6 +5,7 @@ frappe.ui.form.on('Salary Component', {
|
||||
setup: function(frm) {
|
||||
frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 0,
|
||||
|
||||
@@ -303,11 +303,11 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if self.salary_structure:
|
||||
self.calculate_component_amounts("deductions")
|
||||
|
||||
|
||||
self.set_loan_repayment()
|
||||
self.set_component_amounts_based_on_payment_days()
|
||||
self.set_net_pay()
|
||||
|
||||
|
||||
def set_net_pay(self):
|
||||
self.total_deduction = self.get_component_totals("deductions")
|
||||
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
|
||||
@@ -519,7 +519,7 @@ class SalarySlip(TransactionBase):
|
||||
# Total taxable earnings including additional and other incomes
|
||||
total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \
|
||||
+ current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount
|
||||
|
||||
|
||||
# Total taxable earnings without additional earnings with full tax
|
||||
total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax
|
||||
|
||||
@@ -527,7 +527,7 @@ class SalarySlip(TransactionBase):
|
||||
total_structured_tax_amount = self.calculate_tax_by_tax_slab(
|
||||
total_taxable_earnings_without_full_tax_addl_components, tax_slab)
|
||||
current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods
|
||||
|
||||
|
||||
# Total taxable earnings with additional earnings with full tax
|
||||
full_tax_on_additional_earnings = 0.0
|
||||
if current_additional_earnings_with_full_tax:
|
||||
@@ -563,7 +563,7 @@ class SalarySlip(TransactionBase):
|
||||
select sum(sd.amount)
|
||||
from
|
||||
`tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
|
||||
where
|
||||
where
|
||||
sd.parentfield='earnings'
|
||||
and sd.is_tax_applicable=1
|
||||
and is_flexible_benefit=0
|
||||
@@ -676,9 +676,11 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
|
||||
amount, additional_amount = row.amount, row.additional_amount
|
||||
timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
|
||||
|
||||
if (self.salary_structure and
|
||||
cint(row.depends_on_payment_days) and cint(self.total_working_days) and
|
||||
(not self.salary_slip_based_on_timesheet or
|
||||
cint(row.depends_on_payment_days) and cint(self.total_working_days)
|
||||
and (row.salary_component != timesheet_component or
|
||||
getdate(self.start_date) < joining_date or
|
||||
(relieving_date and getdate(self.end_date) > relieving_date)
|
||||
)):
|
||||
@@ -687,14 +689,14 @@ class SalarySlip(TransactionBase):
|
||||
amount = flt((flt(row.default_amount) * flt(self.payment_days)
|
||||
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
|
||||
|
||||
elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
|
||||
elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
|
||||
amount, additional_amount = 0, 0
|
||||
elif not row.amount:
|
||||
amount = flt(row.default_amount) + flt(row.additional_amount)
|
||||
|
||||
# apply rounding
|
||||
if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
|
||||
amount, additional_amount = rounded(amount), rounded(additional_amount)
|
||||
amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0)
|
||||
|
||||
return amount, additional_amount
|
||||
|
||||
@@ -782,7 +784,7 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning:
|
||||
continue
|
||||
|
||||
|
||||
tax_amount += tax_amount * flt(d.percent) / 100
|
||||
|
||||
return tax_amount
|
||||
@@ -869,8 +871,8 @@ class SalarySlip(TransactionBase):
|
||||
`tabRepayment Schedule` as rps, `tabLoan` as l
|
||||
where
|
||||
l.name = rps.parent and rps.payment_date between %s and %s and
|
||||
l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""",
|
||||
(self.start_date, self.end_date, self.employee), as_dict=True) or []
|
||||
l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s and l.company = %s""",
|
||||
(self.start_date, self.end_date, self.employee, self.company), as_dict=True) or []
|
||||
|
||||
def update_salary_slip_in_additional_salary(self):
|
||||
salary_slip = self.name if self.docstatus==1 else None
|
||||
|
||||
@@ -115,6 +115,41 @@ class TestSalarySlip(unittest.TestCase):
|
||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||
{"employee_name":"test_employee@salary.com"}, "name"), "status", "Active")
|
||||
|
||||
def test_payment_days_in_salary_slip_based_on_timesheet(self):
|
||||
from erpnext.projects.doctype.timesheet.test_timesheet import (
|
||||
make_salary_structure_for_timesheet,
|
||||
make_timesheet,
|
||||
)
|
||||
from erpnext.projects.doctype.timesheet.timesheet import (
|
||||
make_salary_slip as make_salary_slip_for_timesheet,
|
||||
)
|
||||
|
||||
# Holidays included in working days
|
||||
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
|
||||
emp = make_employee("test_employee_timesheet1@salary.com", company=erpnext.get_default_company())
|
||||
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
# salary structure based on timesheet
|
||||
make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate=True)
|
||||
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
|
||||
salary_slip.start_date = get_first_day(nowdate())
|
||||
salary_slip.end_date = get_last_day(nowdate())
|
||||
salary_slip.save()
|
||||
salary_slip.submit()
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays)
|
||||
|
||||
# gross pay calculation based on attendance (payment days)
|
||||
gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay))
|
||||
|
||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||
|
||||
def test_employee_salary_slip_read_permission(self):
|
||||
make_employee("test_employee@salary.com")
|
||||
|
||||
@@ -175,7 +210,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# as per assigned salary structure 40500 in monthly salary so 236000*5/100/12
|
||||
frappe.db.sql("""delete from `tabPayroll Period`""")
|
||||
frappe.db.sql("""delete from `tabSalary Component`""")
|
||||
|
||||
|
||||
payroll_period = create_payroll_period()
|
||||
|
||||
create_tax_slab(payroll_period, allow_tax_exemption=True)
|
||||
|
||||
@@ -40,7 +40,7 @@ class StaffingPlan(Document):
|
||||
detail.current_openings = designation_counts['job_openings']
|
||||
|
||||
if detail.number_of_positions > 0:
|
||||
if detail.vacancies > 0 and detail.estimated_cost_per_position:
|
||||
if detail.vacancies and detail.estimated_cost_per_position:
|
||||
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
|
||||
|
||||
self.total_estimated_budget += detail.total_estimated_cost
|
||||
@@ -57,8 +57,7 @@ class StaffingPlan(Document):
|
||||
and sp.to_date >= %s and sp.from_date <= %s and sp.company = %s
|
||||
""", (staffing_plan_detail.designation, self.from_date, self.to_date, self.company))
|
||||
if overlap and overlap [0][0]:
|
||||
frappe.throw(_("Staffing Plan {0} already exist for designation {1}"
|
||||
.format(overlap[0][0], staffing_plan_detail.designation)))
|
||||
frappe.throw(_("Staffing Plan {0} already exist for designation {1}").format(overlap[0][0], staffing_plan_detail.designation))
|
||||
|
||||
def validate_with_parent_plan(self, staffing_plan_detail):
|
||||
if not frappe.get_cached_value('Company', self.company, "parent_company"):
|
||||
@@ -75,12 +74,12 @@ class StaffingPlan(Document):
|
||||
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
|
||||
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
|
||||
for {2} as per staffing plan {3} for parent company {4}."
|
||||
.format(cint(parent_plan_details[0].vacancies),
|
||||
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_plan_details[0].name,
|
||||
parent_company)), ParentCompanyError)
|
||||
parent_company), ParentCompanyError)
|
||||
|
||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
|
||||
@@ -97,14 +96,14 @@ class StaffingPlan(Document):
|
||||
(flt(parent_plan_details[0].total_estimated_cost) < \
|
||||
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
|
||||
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
||||
.format(cint(all_sibling_details.vacancies),
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||
cint(all_sibling_details.vacancies),
|
||||
all_sibling_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_company,
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
parent_plan_details[0].name)))
|
||||
parent_plan_details[0].name))
|
||||
|
||||
def validate_with_subsidiary_plans(self, staffing_plan_detail):
|
||||
#Valdate this plan with all child company plan
|
||||
@@ -120,11 +119,11 @@ class StaffingPlan(Document):
|
||||
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
|
||||
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
||||
.format(self.company,
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||
self.company,
|
||||
cint(children_details.vacancies),
|
||||
children_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
||||
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_designation_counts(designation, company):
|
||||
|
||||
@@ -89,6 +89,7 @@ def execute(filters=None):
|
||||
"amount": salary.net_pay,
|
||||
}
|
||||
data.append(row)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_bank_accounts():
|
||||
@@ -110,7 +111,7 @@ def get_payroll_entries(accounts, filters):
|
||||
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
|
||||
|
||||
payment_accounts = [d.payment_account for d in entries]
|
||||
set_company_account(payment_accounts, entries)
|
||||
entries = set_company_account(payment_accounts, entries)
|
||||
return entries
|
||||
|
||||
def get_salary_slips(payroll_entries):
|
||||
|
||||
@@ -124,11 +124,12 @@ def get_allocated_and_expired_leaves(records, from_date, to_date):
|
||||
def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
|
||||
records= frappe.db.sql("""
|
||||
SELECT
|
||||
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type
|
||||
is_carry_forward, is_expired
|
||||
employee, leave_type, from_date, to_date, leaves, transaction_name,
|
||||
transaction_type, is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||
AND docstatus=1
|
||||
AND transaction_type = 'Leave Allocation'
|
||||
AND (from_date between %(from_date)s AND %(to_date)s
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
|
||||
@@ -138,6 +138,8 @@ def update_employee(employee, details, date=None, cancel=False):
|
||||
new_data = getdate(new_data)
|
||||
elif fieldtype =="Datetime" and new_data:
|
||||
new_data = get_datetime(new_data)
|
||||
elif fieldtype in ["Currency", "Float"] and new_data:
|
||||
new_data = flt(new_data)
|
||||
setattr(employee, item.fieldname, new_data)
|
||||
if item.fieldname in ["department", "designation", "branch"]:
|
||||
internal_work_history[item.fieldname] = item.new
|
||||
|
||||
@@ -27,6 +27,9 @@ frappe.ui.form.on('Maintenance Visit', {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value({mntc_date: frappe.datetime.get_today()});
|
||||
}
|
||||
if (frm.doc.purposes.length && frm.doc.purposes[0].item_name == undefined) {
|
||||
frm.clear_table("purposes");
|
||||
}
|
||||
},
|
||||
customer: function(frm) {
|
||||
erpnext.utils.get_party_details(frm);
|
||||
@@ -49,13 +52,17 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
|
||||
|
||||
if (this.frm.doc.docstatus===0) {
|
||||
this.frm.add_custom_button(__('Maintenance Schedule'),
|
||||
function() {
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.msgprint(__('Please select Customer first'));
|
||||
return;
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
|
||||
source_doctype: "Maintenance Schedule",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
customer: me.frm.doc.customer || undefined,
|
||||
customer: me.frm.doc.customer,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
@@ -80,13 +87,17 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
|
||||
})
|
||||
}, __("Get items from"));
|
||||
this.frm.add_custom_button(__('Sales Order'),
|
||||
function() {
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.msgprint(__('Please select Customer first'));
|
||||
return;
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_visit",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
customer: me.frm.doc.customer || undefined,
|
||||
customer: me.frm.doc.customer,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
@@ -99,4 +110,4 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
|
||||
},
|
||||
});
|
||||
|
||||
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({frm: cur_frm}));
|
||||
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({frm: cur_frm}));
|
||||
|
||||
@@ -240,6 +240,9 @@ class BOM(WebsiteGenerator):
|
||||
existing_bom_cost = self.total_cost
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.item_code:
|
||||
continue
|
||||
|
||||
rate = self.get_rm_rate({
|
||||
"company": self.company,
|
||||
"item_code": d.item_code,
|
||||
@@ -414,25 +417,29 @@ class BOM(WebsiteGenerator):
|
||||
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
|
||||
check_list.append(m)
|
||||
|
||||
def check_recursion(self, bom_list=[]):
|
||||
def check_recursion(self, bom_list=None):
|
||||
""" Check whether recursion occurs in any bom"""
|
||||
def _throw_error(bom_name):
|
||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
|
||||
|
||||
bom_list = self.traverse_tree()
|
||||
bom_nos = frappe.get_all('BOM Item', fields=["bom_no"],
|
||||
filters={'parent': ('in', bom_list), 'parenttype': 'BOM'})
|
||||
child_items = frappe.get_all('BOM Item', fields=["bom_no", "item_code"],
|
||||
filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) or []
|
||||
|
||||
raise_exception = False
|
||||
if bom_nos and self.name in [d.bom_no for d in bom_nos]:
|
||||
raise_exception = True
|
||||
child_bom = {d.bom_no for d in child_items}
|
||||
child_items_codes = {d.item_code for d in child_items}
|
||||
|
||||
if not raise_exception:
|
||||
bom_nos = frappe.get_all('BOM Item', fields=["parent"],
|
||||
filters={'bom_no': self.name, 'parenttype': 'BOM'})
|
||||
if self.name in child_bom:
|
||||
_throw_error(self.name)
|
||||
|
||||
if self.name in [d.parent for d in bom_nos]:
|
||||
raise_exception = True
|
||||
if self.item in child_items_codes:
|
||||
_throw_error(self.item)
|
||||
|
||||
if raise_exception:
|
||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name))
|
||||
bom_nos = frappe.get_all('BOM Item', fields=["parent"],
|
||||
filters={'bom_no': self.name, 'parenttype': 'BOM'}) or []
|
||||
|
||||
if self.name in {d.parent for d in bom_nos}:
|
||||
_throw_error(self.name)
|
||||
|
||||
def update_cost_and_exploded_items(self, bom_list=[]):
|
||||
bom_list = self.traverse_tree(bom_list)
|
||||
@@ -545,7 +552,7 @@ class BOM(WebsiteGenerator):
|
||||
for d in self.get('items'):
|
||||
if d.bom_no:
|
||||
self.get_child_exploded_items(d.bom_no, d.stock_qty)
|
||||
else:
|
||||
elif d.item_code:
|
||||
self.add_to_cur_exploded_items(frappe._dict({
|
||||
'item_code' : d.item_code,
|
||||
'item_name' : d.item_name,
|
||||
|
||||
@@ -228,7 +228,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if self.total_produced_qty > 0:
|
||||
self.status = "In Process"
|
||||
if self.total_produced_qty == self.total_planned_qty:
|
||||
if self.total_produced_qty >= self.total_planned_qty:
|
||||
self.status = "Completed"
|
||||
|
||||
if self.status != 'Completed':
|
||||
|
||||
@@ -198,6 +198,7 @@ class TestProductionPlan(unittest.TestCase):
|
||||
pln.cancel()
|
||||
frappe.delete_doc("Production Plan", pln.name)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.production_item",
|
||||
"fetch_from": "production_item.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
@@ -470,7 +472,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2019-12-04 11:20:04.695123",
|
||||
"modified": "2021-08-16 11:20:04.695123",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -88,7 +88,7 @@ def get_bom_stock(filters):
|
||||
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
|
||||
|
||||
def get_manufacturer_records():
|
||||
details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
|
||||
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
|
||||
manufacture_details = frappe._dict()
|
||||
for detail in details:
|
||||
dic = manufacture_details.setdefault(detail.get('parent'), {})
|
||||
|
||||
@@ -686,3 +686,4 @@ erpnext.patches.v12_0.purchase_receipt_status
|
||||
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
|
||||
erpnext.patches.v12_0.create_taxable_value_field_in_purchase_invoice
|
||||
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
|
||||
|
||||
@@ -6,6 +6,9 @@ import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
def execute():
|
||||
# updating column value to handle field change from Data to Currency
|
||||
frappe.db.sql("update `tabBOM` set base_scrap_material_cost = '0' where trim(coalesce(base_scrap_material_cost, ''))= ''")
|
||||
|
||||
for doctype in ['BOM Explosion Item', 'BOM Item', 'Work Order Item', 'Item']:
|
||||
if frappe.db.has_column(doctype, 'allow_transfer_for_manufacture'):
|
||||
if doctype != 'Item':
|
||||
@@ -26,4 +29,4 @@ def execute():
|
||||
else:
|
||||
frappe.db.sql(""" UPDATE `tab%s`
|
||||
SET transfer_material_against = 'Work Order'
|
||||
WHERE docstatus < 2""" % (doctype))
|
||||
WHERE docstatus < 2""" % (doctype))
|
||||
|
||||
12
erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
Normal file
12
erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||
if not company:
|
||||
return
|
||||
|
||||
irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
|
||||
if irn_cancelled_field:
|
||||
frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
|
||||
frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)
|
||||
@@ -86,6 +86,10 @@ class Project(Document):
|
||||
if self.sales_order:
|
||||
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
|
||||
|
||||
def on_trash(self):
|
||||
for so in frappe.get_all("Sales Order", {"project": self.name}, ["name"]):
|
||||
frappe.db.set_value("Sales Order", so.get('name'), "project", "")
|
||||
|
||||
def update_percent_complete(self):
|
||||
if self.percent_complete_method == "Manual":
|
||||
if self.status == "Completed":
|
||||
|
||||
@@ -8,7 +8,8 @@ test_records = frappe.get_test_records('Project')
|
||||
test_ignore = ["Sales Order"]
|
||||
|
||||
from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
|
||||
from erpnext.projects.doctype.project.project import set_project_status
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
from frappe.utils import getdate
|
||||
|
||||
@@ -32,6 +33,21 @@ class TestProject(unittest.TestCase):
|
||||
self.assertEqual(task4.subject, 'Task 4')
|
||||
self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
|
||||
|
||||
def test_project_linking_with_sales_order(self):
|
||||
so = make_sales_order()
|
||||
project = make_project_from_so(so.name)
|
||||
|
||||
project.save()
|
||||
self.assertEqual(project.sales_order, so.name)
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.project, project.name)
|
||||
|
||||
project.delete()
|
||||
|
||||
so.reload()
|
||||
self.assertFalse(so.project)
|
||||
|
||||
def get_project(name):
|
||||
template = get_project_template()
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ class Task(NestedSet):
|
||||
if (self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
|
||||
if self.progress == 100:
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status == 'Completed':
|
||||
self.progress = 100
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ class TestTimesheet(unittest.TestCase):
|
||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
|
||||
|
||||
|
||||
def test_timesheet_billing_amount(self):
|
||||
make_salary_structure_for_timesheet("_T-Employee-00001")
|
||||
timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1)
|
||||
@@ -177,6 +173,9 @@ def make_salary_structure_for_timesheet(employee):
|
||||
salary_structure_name = "Timesheet Salary Structure Test"
|
||||
frequency = "Monthly"
|
||||
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
|
||||
|
||||
salary_structure = make_salary_structure(salary_structure_name, frequency, dont_submit=True)
|
||||
salary_structure.salary_component = "Timesheet Component"
|
||||
salary_structure.salary_slip_based_on_timesheet = 1
|
||||
|
||||
@@ -940,7 +940,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||
if(d.charge_type == "Actual") {
|
||||
frappe.model.set_value(d.doctype, d.name, "tax_amount",
|
||||
flt(d.tax_amount) / flt(exchange_rate));
|
||||
flt(d.base_tax_amount) / flt(exchange_rate));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -550,7 +550,7 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
const trans_items = this.get_values()["trans_items"];
|
||||
const trans_items = this.get_values()["trans_items"].filter((item) => !!item.item_code);
|
||||
frappe.call({
|
||||
method: 'erpnext.controllers.accounts_controller.update_child_qty_rate',
|
||||
freeze: true,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"section_break_2",
|
||||
"sandbox_mode",
|
||||
"credentials",
|
||||
"advanced_settings_section",
|
||||
"client_id",
|
||||
"column_break_8",
|
||||
"client_secret",
|
||||
"auth_token",
|
||||
"token_expiry"
|
||||
],
|
||||
@@ -48,12 +52,32 @@
|
||||
"fieldname": "sandbox_mode",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sandbox Mode"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "advanced_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Client ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "Client Secret"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-13 12:04:49.449199",
|
||||
"modified": "2021-11-16 19:50:28.029517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E Invoice Settings",
|
||||
|
||||
@@ -23,9 +23,5 @@
|
||||
"StateCesAmt": "{item.state_cess_amount}",
|
||||
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
|
||||
"OthChrg": "{item.other_charges}",
|
||||
"TotItemVal": "{item.total_value}",
|
||||
"BchDtls": {{
|
||||
"Nm": "{item.batch_no}",
|
||||
"ExpDt": "{item.batch_expiry_date}"
|
||||
}}
|
||||
"TotItemVal": "{item.total_value}"
|
||||
}}
|
||||
@@ -1,6 +1,8 @@
|
||||
erpnext.setup_einvoice_actions = (doctype) => {
|
||||
frappe.ui.form.on(doctype, {
|
||||
async refresh(frm) {
|
||||
if (frm.doc.docstatus == 2) return;
|
||||
|
||||
const res = await frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
|
||||
args: { doc: frm.doc }
|
||||
@@ -115,7 +117,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const action = () => {
|
||||
let message = __('Cancellation of e-way bill is currently not supported. ');
|
||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
|
||||
|
||||
@@ -36,11 +36,16 @@ def validate_eligibility(doc):
|
||||
return False
|
||||
|
||||
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
|
||||
invalid_company_gstin = not frappe.db.get_value('E Invoice User', {'gstin': doc.get('company_gstin')})
|
||||
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
no_taxes_applied = not doc.get('taxes')
|
||||
|
||||
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
# if export invoice, then taxes can be empty
|
||||
# invoice can only be ineligible if no taxes applied and is not an export invoice
|
||||
no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
|
||||
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
|
||||
|
||||
if invalid_company or invalid_company_gstin or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -191,18 +196,12 @@ def get_item_list(invoice):
|
||||
|
||||
item.qty = abs(item.qty)
|
||||
|
||||
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||
item.discount_amount = abs(item.base_amount - item.base_net_amount)
|
||||
else:
|
||||
item.discount_amount = 0
|
||||
|
||||
item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
|
||||
item.gross_amount = abs(item.taxable_value) + item.discount_amount
|
||||
item.unit_rate = abs(item.taxable_value / item.qty)
|
||||
item.gross_amount = abs(item.taxable_value)
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
item.discount_amount = 0
|
||||
|
||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
|
||||
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
|
||||
item.serial_no = ""
|
||||
|
||||
item = update_item_taxes(invoice, item)
|
||||
@@ -254,18 +253,8 @@ def update_item_taxes(invoice, item):
|
||||
|
||||
def get_invoice_value_details(invoice):
|
||||
invoice_value_details = frappe._dict(dict())
|
||||
|
||||
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||
# Discount already applied on net total which means on items
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
invoice_value_details.invoice_discount_amt = 0
|
||||
elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount:
|
||||
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
else:
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
# since tax already considers discount amount
|
||||
invoice_value_details.invoice_discount_amt = 0
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
invoice_value_details.invoice_discount_amt = 0
|
||||
|
||||
invoice_value_details.round_off = invoice.base_rounding_adjustment
|
||||
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
|
||||
@@ -287,8 +276,8 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
||||
considered_rows = []
|
||||
|
||||
for t in invoice.taxes:
|
||||
tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \
|
||||
else t.base_tax_amount_after_discount_amount
|
||||
tax_amount = t.base_tax_amount_after_discount_amount
|
||||
|
||||
if t.account_head in gst_accounts_list:
|
||||
if t.account_head in gst_accounts.cess_account:
|
||||
# using after discount amt since item also uses after discount amt for cess calc
|
||||
@@ -318,7 +307,7 @@ def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invo
|
||||
|
||||
def get_payment_details(invoice):
|
||||
payee_name = invoice.company
|
||||
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
||||
mode_of_payment = ""
|
||||
paid_amount = invoice.base_paid_amount
|
||||
outstanding_amount = invoice.outstanding_amount
|
||||
|
||||
@@ -469,10 +458,16 @@ def make_einvoice(invoice):
|
||||
try:
|
||||
einvoice = safe_json_load(einvoice)
|
||||
einvoice = santize_einvoice_fields(einvoice)
|
||||
except json.JSONDecodeError:
|
||||
raise
|
||||
except Exception:
|
||||
show_link_to_error_log(invoice, einvoice)
|
||||
|
||||
validate_totals(einvoice)
|
||||
try:
|
||||
validate_totals(einvoice)
|
||||
except Exception:
|
||||
log_error(einvoice)
|
||||
raise
|
||||
|
||||
return einvoice
|
||||
|
||||
@@ -544,7 +539,14 @@ def safe_json_load(json_string):
|
||||
pos = e.pos
|
||||
start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
|
||||
snippet = json_string[start:end]
|
||||
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Error in input data. Please check for any special characters near following input: <br> {}"
|
||||
).format(snippet),
|
||||
title=_("Invalid JSON"),
|
||||
exc=e,
|
||||
)
|
||||
|
||||
|
||||
def throw_error_list(errors, title):
|
||||
if len(errors) > 1:
|
||||
@@ -621,10 +623,17 @@ class GSPConnector():
|
||||
request_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def get_client_credentials(self):
|
||||
if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret:
|
||||
return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret')
|
||||
|
||||
return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret
|
||||
|
||||
def fetch_auth_token(self):
|
||||
client_id, client_secret = self.get_client_credentials()
|
||||
headers = {
|
||||
'gspappid': frappe.conf.einvoice_client_id,
|
||||
'gspappsecret': frappe.conf.einvoice_client_secret
|
||||
'gspappid': client_id,
|
||||
'gspappsecret': client_secret
|
||||
}
|
||||
res = {}
|
||||
try:
|
||||
@@ -767,12 +776,13 @@ class GSPConnector():
|
||||
|
||||
headers = self.get_headers()
|
||||
eway_bill_details = get_eway_bill_details(args)
|
||||
|
||||
data = json.dumps({
|
||||
'Irn': args.irn,
|
||||
'Distance': cint(eway_bill_details.distance),
|
||||
'TransMode': eway_bill_details.mode_of_transport,
|
||||
'TransId': eway_bill_details.gstin,
|
||||
'TransName': eway_bill_details.transporter,
|
||||
'TransName': eway_bill_details.name,
|
||||
'TrnDocDt': eway_bill_details.document_date,
|
||||
'TrnDocNo': eway_bill_details.document_name,
|
||||
'VehNo': eway_bill_details.vehicle_no,
|
||||
@@ -864,7 +874,7 @@ class GSPConnector():
|
||||
if errors:
|
||||
throw_error_list(errors, title)
|
||||
else:
|
||||
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
|
||||
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed" target="_blank">Error Log</a>'
|
||||
frappe.msgprint(
|
||||
_('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
|
||||
title=title,
|
||||
|
||||
@@ -414,7 +414,7 @@ def make_custom_fields(update=True):
|
||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||
depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'),
|
||||
|
||||
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||
|
||||
@@ -30,12 +30,13 @@ def validate_gstin_for_india(doc, method):
|
||||
|
||||
gst_category = []
|
||||
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
if hasattr(doc, 'gst_category'):
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {'name': link_name}, ['gst_category'])
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {'name': link_name}, ['gst_category'])
|
||||
|
||||
doc.gstin = doc.gstin.upper().strip()
|
||||
if not doc.gstin or doc.gstin == 'NA':
|
||||
@@ -68,10 +69,9 @@ def validate_tax_category(doc, method):
|
||||
def update_gst_category(doc, method):
|
||||
for link in doc.links:
|
||||
if link.link_doctype in ['Customer', 'Supplier']:
|
||||
if doc.get('gstin'):
|
||||
frappe.db.sql("""
|
||||
UPDATE `tab{0}` SET gst_category = %s WHERE name = %s AND gst_category = 'Unregistered'
|
||||
""".format(link.link_doctype), ("Registered Regular", link.link_name)) #nosec
|
||||
meta = frappe.get_meta(link.link_doctype)
|
||||
if doc.get('gstin') and meta.has_field('gst_category'):
|
||||
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
|
||||
|
||||
def set_gst_state_and_state_number(doc):
|
||||
if not doc.gst_state:
|
||||
@@ -856,12 +856,8 @@ def update_taxable_values(doc, method):
|
||||
considered_rows.append(prev_row_id)
|
||||
|
||||
for item in doc.get('items'):
|
||||
if doc.apply_discount_on == 'Grand Total' and doc.discount_amount:
|
||||
proportionate_value = item.base_amount if doc.base_total else item.qty
|
||||
total_value = doc.base_total if doc.base_total else doc.total_qty
|
||||
else:
|
||||
proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
|
||||
total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
|
||||
proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
|
||||
total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
|
||||
|
||||
applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)),
|
||||
item.precision('taxable_value')))
|
||||
|
||||
@@ -104,14 +104,14 @@ def set_address_details(row, special_characters):
|
||||
row.update({'ship_to_state': row.to_state})
|
||||
|
||||
def set_taxes(row, filters):
|
||||
taxes = frappe.get_list("Sales Taxes and Charges",
|
||||
taxes = frappe.get_all("Sales Taxes and Charges",
|
||||
filters={
|
||||
'parent': row.dn_id
|
||||
},
|
||||
fields=('item_wise_tax_detail', 'account_head'))
|
||||
|
||||
account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"]
|
||||
taxes_list = frappe.get_list("GST Account",
|
||||
taxes_list = frappe.get_all("GST Account",
|
||||
filters={
|
||||
"parent": "GST Settings",
|
||||
"company": filters.company
|
||||
|
||||
@@ -714,8 +714,7 @@ def make_maintenance_schedule(source_name, target_doc=None):
|
||||
"doctype": "Maintenance Schedule Item",
|
||||
"field_map": {
|
||||
"parent": "sales_order"
|
||||
},
|
||||
"add_if_empty": True
|
||||
}
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
@@ -741,8 +740,7 @@ def make_maintenance_visit(source_name, target_doc=None):
|
||||
"field_map": {
|
||||
"parent": "prevdoc_docname",
|
||||
"parenttype": "prevdoc_doctype"
|
||||
},
|
||||
"add_if_empty": True
|
||||
}
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
|
||||
@@ -62,12 +62,12 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
|
||||
self.assertEqual(exchange_rate, 62.9)
|
||||
|
||||
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
|
||||
|
||||
# Exchange rate as on 15th Dec, 2015
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
|
||||
self.assertFalse(exchange_rate == 60)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.894)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.999)
|
||||
|
||||
def test_exchange_rate_strict(self):
|
||||
# strict currency settings
|
||||
@@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
|
||||
self.assertEqual(exchange_rate, 60.0)
|
||||
|
||||
# Will fetch from fixer.io
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.79)
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.235)
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
|
||||
self.assertEqual(exchange_rate, 62.9)
|
||||
|
||||
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
|
||||
# Exchange rate as on 15th Dec, 2015
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.894)
|
||||
|
||||
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling")
|
||||
self.assertEqual(exchange_rate, 65.1)
|
||||
|
||||
# NGN is not available on fixer.io so these should return 0
|
||||
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling")
|
||||
self.assertEqual(exchange_rate, 0)
|
||||
|
||||
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling")
|
||||
self.assertEqual(exchange_rate, 0)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.999)
|
||||
|
||||
def test_exchange_rate_strict_switched(self):
|
||||
# Start with allow_stale is True
|
||||
@@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
# Will fetch from fixer.io
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.79)
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.235)
|
||||
|
||||
@@ -33,6 +33,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
self.parent_item_group = _('All Item Groups')
|
||||
|
||||
self.make_route()
|
||||
self.validate_item_group_defaults()
|
||||
|
||||
def on_update(self):
|
||||
NestedSet.on_update(self)
|
||||
@@ -88,6 +89,10 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
def delete_child_item_groups_key(self):
|
||||
frappe.cache().hdel("child_item_groups", self.name)
|
||||
|
||||
def validate_item_group_defaults(self):
|
||||
from erpnext.stock.doctype.item.item import validate_item_default_company_links
|
||||
validate_item_default_company_links(self.item_group_defaults)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
|
||||
if product_group:
|
||||
|
||||
@@ -1,73 +1,74 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group",
|
||||
"parent_item_group": "All Item Groups",
|
||||
"item_group_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
||||
"selling_cost_center": "_Test Cost Center 2 - _TC"
|
||||
"selling_cost_center": "_Test Cost Center 2 - _TC",
|
||||
"default_warehouse": "_Test Warehouse - _TC"
|
||||
}]
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group Desktops",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group Desktops",
|
||||
"parent_item_group": "All Item Groups"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group A",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group A",
|
||||
"parent_item_group": "All Item Groups"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B",
|
||||
"parent_item_group": "All Item Groups"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B - 1",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B - 1",
|
||||
"parent_item_group": "_Test Item Group B"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B - 2",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B - 2",
|
||||
"parent_item_group": "_Test Item Group B"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group B - 3",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 0,
|
||||
"item_group_name": "_Test Item Group B - 3",
|
||||
"parent_item_group": "_Test Item Group B"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C",
|
||||
"parent_item_group": "All Item Groups"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C - 1",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C - 1",
|
||||
"parent_item_group": "_Test Item Group C"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C - 2",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group C - 2",
|
||||
"parent_item_group": "_Test Item Group C"
|
||||
},
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group D",
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group D",
|
||||
"parent_item_group": "All Item Groups"
|
||||
},
|
||||
{
|
||||
@@ -104,4 +105,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -175,9 +175,9 @@ def install(country=None):
|
||||
]},
|
||||
|
||||
# Issue Priority
|
||||
{'doctype': 'Issue Priority', 'name': _('Low')},
|
||||
{'doctype': 'Issue Priority', 'name': _('Medium')},
|
||||
{'doctype': 'Issue Priority', 'name': _('High')},
|
||||
{'doctype': 'Issue Priority', 'name': 'Low'},
|
||||
{'doctype': 'Issue Priority', 'name': 'Medium'},
|
||||
{'doctype': 'Issue Priority', 'name': 'High'},
|
||||
|
||||
#Job Applicant Source
|
||||
{'doctype': 'Job Applicant Source', 'source_name': _('Website Listing')},
|
||||
|
||||
@@ -93,20 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
|
||||
try:
|
||||
cache = frappe.cache()
|
||||
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency)
|
||||
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
|
||||
value = cache.get(key)
|
||||
|
||||
if not value:
|
||||
import requests
|
||||
api_url = "https://frankfurter.app/{0}".format(transaction_date)
|
||||
api_url = "https://api.exchangerate.host/convert"
|
||||
response = requests.get(api_url, params={
|
||||
"base": from_currency,
|
||||
"symbols": to_currency
|
||||
"date": transaction_date,
|
||||
"from": from_currency,
|
||||
"to": to_currency
|
||||
})
|
||||
# expire in 6 hours
|
||||
response.raise_for_status()
|
||||
value = response.json()["rates"][to_currency]
|
||||
cache.setex(key, value, 6 * 60 * 60)
|
||||
value = response.json()["result"]
|
||||
cache.setex(name=key, time=21600, value=flt(value))
|
||||
return flt(value)
|
||||
except:
|
||||
frappe.log_error(title="Get Exchange Rate")
|
||||
|
||||
@@ -175,7 +175,9 @@ def add_new_address(doc):
|
||||
def create_lead_for_item_inquiry(lead, subject, message):
|
||||
lead = frappe.parse_json(lead)
|
||||
lead_doc = frappe.new_doc('Lead')
|
||||
lead_doc.update(lead)
|
||||
for fieldname in ("lead_name", "company_name", "email_id", "phone"):
|
||||
lead_doc.set(fieldname, lead.get(fieldname))
|
||||
|
||||
lead_doc.set('lead_owner', '')
|
||||
|
||||
try:
|
||||
@@ -206,11 +208,11 @@ def update_cart_address(address_type, address_name):
|
||||
if address_type.lower() == "billing":
|
||||
quotation.customer_address = address_name
|
||||
quotation.address_display = address_display
|
||||
quotation.shipping_address_name == quotation.shipping_address_name or address_name
|
||||
quotation.shipping_address_name = quotation.shipping_address_name or address_name
|
||||
elif address_type.lower() == "shipping":
|
||||
quotation.shipping_address_name = address_name
|
||||
quotation.shipping_address = address_display
|
||||
quotation.customer_address == quotation.customer_address or address_name
|
||||
quotation.customer_address = quotation.customer_address or address_name
|
||||
|
||||
apply_cart_settings(quotation=quotation)
|
||||
|
||||
@@ -279,7 +281,7 @@ def update_party(fullname, company_name=None, mobile_no=None, phone=None):
|
||||
party = get_party()
|
||||
|
||||
party.customer_name = company_name or fullname
|
||||
party.customer_type == "Company" if company_name else "Individual"
|
||||
party.customer_type = "Company" if company_name else "Individual"
|
||||
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
|
||||
@@ -85,7 +85,7 @@ frappe.ui.form.on("Item", {
|
||||
}
|
||||
if (frm.doc.variant_of) {
|
||||
frm.set_intro(__('This Item is a Variant of {0} (Template).',
|
||||
[`<a href="/app/item/${frm.doc.variant_of}" onclick="location.reload()">${frm.doc.variant_of}</a>`]), true);
|
||||
[`<a href="#Form/Item/${frm.doc.variant_of}" target="_blank">${frm.doc.variant_of}</a>`]), true);
|
||||
}
|
||||
|
||||
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
||||
import itertools
|
||||
import json
|
||||
import erpnext
|
||||
|
||||
import frappe
|
||||
import copy
|
||||
from erpnext.controllers.item_variant import (ItemVariantExistsError,
|
||||
@@ -121,9 +122,9 @@ class Item(WebsiteGenerator):
|
||||
self.validate_fixed_asset()
|
||||
self.validate_retain_sample()
|
||||
self.validate_uom_conversion_factor()
|
||||
self.validate_item_defaults()
|
||||
self.validate_customer_provided_part()
|
||||
self.update_defaults_from_item_group()
|
||||
self.validate_item_defaults()
|
||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||
self.cant_change()
|
||||
self.update_show_in_website()
|
||||
@@ -205,10 +206,11 @@ class Item(WebsiteGenerator):
|
||||
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
|
||||
|
||||
def validate_website_image(self):
|
||||
"""Validate if the website image is a public file"""
|
||||
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
"""Validate if the website image is a public file"""
|
||||
auto_set_website_image = False
|
||||
if not self.website_image and self.image:
|
||||
auto_set_website_image = True
|
||||
@@ -238,10 +240,11 @@ class Item(WebsiteGenerator):
|
||||
self.website_image = None
|
||||
|
||||
def make_thumbnail(self):
|
||||
"""Make a thumbnail of `website_image`"""
|
||||
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
"""Make a thumbnail of `website_image`"""
|
||||
import requests.exceptions
|
||||
|
||||
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
|
||||
@@ -687,7 +690,6 @@ class Item(WebsiteGenerator):
|
||||
|
||||
def recalculate_bin_qty(self, new_name):
|
||||
from erpnext.stock.stock_balance import repost_stock
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
@@ -701,7 +703,6 @@ class Item(WebsiteGenerator):
|
||||
repost_stock(new_name, warehouse)
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
def copy_specification_from_item_group(self):
|
||||
self.set("website_specifications", [])
|
||||
@@ -758,35 +759,39 @@ class Item(WebsiteGenerator):
|
||||
if len(companies) != len(self.item_defaults):
|
||||
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
|
||||
|
||||
validate_item_default_company_links(self.item_defaults)
|
||||
|
||||
|
||||
def update_defaults_from_item_group(self):
|
||||
"""Get defaults from Item Group"""
|
||||
if self.item_group and not self.item_defaults:
|
||||
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
||||
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
|
||||
'expense_account','selling_cost_center','income_account'], as_dict = 1)
|
||||
if item_defaults:
|
||||
for item in item_defaults:
|
||||
self.append('item_defaults', {
|
||||
'company': item.company,
|
||||
'default_warehouse': item.default_warehouse,
|
||||
'default_price_list': item.default_price_list,
|
||||
'buying_cost_center': item.buying_cost_center,
|
||||
'default_supplier': item.default_supplier,
|
||||
'expense_account': item.expense_account,
|
||||
'selling_cost_center': item.selling_cost_center,
|
||||
'income_account': item.income_account
|
||||
})
|
||||
else:
|
||||
warehouse = ''
|
||||
defaults = frappe.defaults.get_defaults() or {}
|
||||
if self.item_defaults or not self.item_group:
|
||||
return
|
||||
|
||||
# To check default warehouse is belong to the default company
|
||||
if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
|
||||
{'name': defaults.default_warehouse, 'company': defaults.company}):
|
||||
self.append("item_defaults", {
|
||||
"company": defaults.get("company"),
|
||||
"default_warehouse": defaults.default_warehouse
|
||||
})
|
||||
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
||||
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
|
||||
'expense_account','selling_cost_center','income_account'], as_dict = 1)
|
||||
if item_defaults:
|
||||
for item in item_defaults:
|
||||
self.append('item_defaults', {
|
||||
'company': item.company,
|
||||
'default_warehouse': item.default_warehouse,
|
||||
'default_price_list': item.default_price_list,
|
||||
'buying_cost_center': item.buying_cost_center,
|
||||
'default_supplier': item.default_supplier,
|
||||
'expense_account': item.expense_account,
|
||||
'selling_cost_center': item.selling_cost_center,
|
||||
'income_account': item.income_account
|
||||
})
|
||||
else:
|
||||
defaults = frappe.defaults.get_defaults() or {}
|
||||
|
||||
# To check default warehouse is belong to the default company
|
||||
if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
|
||||
{'name': defaults.default_warehouse, 'company': defaults.company}):
|
||||
self.append("item_defaults", {
|
||||
"company": defaults.get("company"),
|
||||
"default_warehouse": defaults.default_warehouse
|
||||
})
|
||||
|
||||
def update_variants(self):
|
||||
if self.flags.dont_update_variants or \
|
||||
@@ -1237,3 +1242,24 @@ def update_variants(variants, template, publish_progress=True):
|
||||
def on_doctype_update():
|
||||
# since route is a Text column, it needs a length for indexing
|
||||
frappe.db.add_index("Item", ["route(500)"])
|
||||
|
||||
def validate_item_default_company_links(item_defaults):
|
||||
for item_default in item_defaults:
|
||||
for doctype, field in [
|
||||
['Warehouse', 'default_warehouse'],
|
||||
['Cost Center', 'buying_cost_center'],
|
||||
['Cost Center', 'selling_cost_center'],
|
||||
['Account', 'expense_account'],
|
||||
['Account', 'income_account']
|
||||
]:
|
||||
if item_default.get(field):
|
||||
company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True)
|
||||
if company and company != item_default.company:
|
||||
frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.")
|
||||
.format(
|
||||
item_default.idx,
|
||||
doctype,
|
||||
frappe.bold(item_default.get(field)),
|
||||
frappe.bold(item_default.company),
|
||||
frappe.bold(frappe.unscrub(field))
|
||||
), title=_("Invalid Item Defaults"))
|
||||
|
||||
@@ -219,6 +219,23 @@ class TestItem(unittest.TestCase):
|
||||
for key, value in iteritems(purchase_item_check):
|
||||
self.assertEqual(value, purchase_item_details.get(key))
|
||||
|
||||
def test_item_default_validations(self):
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as ve:
|
||||
make_item("Bad Item defaults", {
|
||||
"item_group": "_Test Item Group",
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company 1",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"expense_account": "Stock In Hand - _TC",
|
||||
"buying_cost_center": "_Test Cost Center - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
}]
|
||||
})
|
||||
|
||||
self.assertTrue("belong to company" in str(ve.exception).lower(),
|
||||
msg="Mismatching company entities in item defaults should not be allowed.")
|
||||
|
||||
def test_item_attribute_change_after_variant(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||
|
||||
@@ -451,7 +468,7 @@ class TestItem(unittest.TestCase):
|
||||
item_doc.save()
|
||||
|
||||
# Check values saved correctly
|
||||
barcodes = frappe.get_list(
|
||||
barcodes = frappe.get_all(
|
||||
'Item Barcode',
|
||||
fields=['barcode', 'barcode_type'],
|
||||
filters={'parent': item_code})
|
||||
|
||||
@@ -69,7 +69,8 @@ frappe.ui.form.on('Material Request', {
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
|
||||
if (flt(frm.doc.per_ordered, 2) < 100) {
|
||||
let precision = frappe.defaults.get_default("float_precision");
|
||||
if (flt(frm.doc.per_ordered, precision) < 100) {
|
||||
let add_create_pick_list_button = () => {
|
||||
frm.add_custom_button(__('Pick List'),
|
||||
() => frm.events.create_pick_list(frm), __('Create'));
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
frappe.listview_settings['Material Request'] = {
|
||||
add_fields: ["material_request_type", "status", "per_ordered", "per_received"],
|
||||
get_indicator: function(doc) {
|
||||
if(doc.status=="Stopped") {
|
||||
var precision = frappe.defaults.get_default("float_precision");
|
||||
if (doc.status=="Stopped") {
|
||||
return [__("Stopped"), "red", "status,=,Stopped"];
|
||||
} else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) {
|
||||
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) {
|
||||
return [__("Pending"), "orange", "per_ordered,=,0"];
|
||||
} else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) {
|
||||
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) {
|
||||
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
|
||||
} else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) {
|
||||
if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) {
|
||||
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) {
|
||||
if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) {
|
||||
return [__("Partially Received"), "yellow", "per_received,<,100"];
|
||||
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) == 100) {
|
||||
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
|
||||
return [__("Received"), "green", "per_received,=,100"];
|
||||
} else if (doc.material_request_type == "Purchase") {
|
||||
return [__("Ordered"), "green", "per_ordered,=,100"];
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty"
|
||||
"label": "Qty",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "picked_qty",
|
||||
@@ -180,7 +181,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-13 19:08:21.995986",
|
||||
"modified": "2021-09-28 12:02:16.923056",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
@@ -190,4 +191,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Stock Entry': 'Return',
|
||||
'Purchase Invoice': 'Purchase Invoice'
|
||||
@@ -34,7 +34,7 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
filters: {'company': frm.doc.company }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
onload: function(frm) {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
@@ -107,6 +107,8 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
|
||||
message: __("Please Select a Supplier")
|
||||
});
|
||||
}
|
||||
|
||||
me.frm.doc.taxes = [];
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
|
||||
source_doctype: "Purchase Order",
|
||||
|
||||
@@ -78,7 +78,11 @@ frappe.ui.form.on('Stock Entry', {
|
||||
}
|
||||
}
|
||||
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
// User could want to select a manually created empty batch (no warehouse)
|
||||
// or a pre-existing batch
|
||||
if (frm.doc.purpose != "Material Receipt") {
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
}
|
||||
|
||||
return {
|
||||
query : "erpnext.controllers.queries.get_batch_no",
|
||||
@@ -282,6 +286,12 @@ frappe.ui.form.on('Stock Entry', {
|
||||
frm.trigger("setup_quality_inspection");
|
||||
},
|
||||
|
||||
before_save: function(frm) {
|
||||
frm.doc.items.forEach((item) => {
|
||||
item.uom = item.uom || item.stock_uom;
|
||||
})
|
||||
},
|
||||
|
||||
purpose: function(frm) {
|
||||
frm.trigger('validate_purpose_consumption');
|
||||
frm.fields_dict.items.grid.refresh();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user