Compare commits

...

274 Commits

Author SHA1 Message Date
Deepesh Garg
65771b431b fix: Reorder patch execution 2021-05-05 15:04:40 +05:30
Deepesh Garg
25e4af5b03 fix: Inward supplies liable to reverse charge 2021-05-05 14:24:37 +05:30
Deepesh Garg
ae04395caf fix: Add taxable value in Purchase Invoice Item 2021-05-05 14:24:31 +05:30
Deepesh Garg
11e4760530 fix: Add method to get invoices liable to reverse charge 2021-05-05 14:23:30 +05:30
Deepesh Garg
171519a4b8 fix: GSTR 3B report cleanup and updates 2021-05-05 14:22:48 +05:30
Deepesh Garg
984d6b2d39 fix: Setup and patch 2021-05-05 14:22:05 +05:30
Deepesh Garg
4f6b760cfd fix: Debit note using Sales Invoice 2021-05-05 14:22:03 +05:30
Deepesh Garg
ca8fe28138 fix: Update params in GSTR-1 report 2021-05-05 14:02:51 +05:30
Deepesh Garg
5b9e0b5b57 fix: Cleanup and fixes in GSTR3B report 2021-05-05 14:02:51 +05:30
Deepesh Garg
c7349145c5 fix: Hooks method to update availed ITC field 2021-05-05 14:02:50 +05:30
Deepesh Garg
3875f23d9a fix: Addd patch for availed ITC fields 2021-05-05 14:02:48 +05:30
Deepesh Garg
b5ea536530 fix: Reverse Charge booking logic and validation 2021-05-05 14:01:52 +05:30
Deepesh Garg
ac61a86021 feat(India): ITC Reversal via Journal Entry 2021-05-05 14:01:07 +05:30
Deepesh Garg
a5b7d5ecf0 fix: RCM tax calculation 2021-05-05 13:59:28 +05:30
Saqib Ansari
fda58abfaf fix: sider issues 2021-05-01 19:22:14 +05:30
Saqib
86e700b24f fix: remove print statement 2021-05-01 19:22:01 +05:30
Saqib Ansari
80b57e7c41 refactor: remove extra fields 2021-05-01 19:21:47 +05:30
Saqib Ansari
900adeefda fix: test 2021-05-01 19:21:38 +05:30
Saqib Ansari
36c9a8ab08 feat: set dynamic labels for payment schedule fields 2021-04-27 13:01:19 +05:30
Saqib Ansari
8288b5a302 feat: base payment amount in payment schedule 2021-04-27 13:00:23 +05:30
Saqib Ansari
d27e663b2a fix: payment amount showing in foreign currency 2021-04-27 13:00:13 +05:30
Saqib Ansari
a82ecf8c07 chore: add discount account company filter 2021-04-27 12:59:34 +05:30
Saqib Ansari
427f9b6367 feat: tests for discounted payment terms 2021-04-27 12:59:34 +05:30
Saqib Ansari
e42f423ed8 fix: travis 2021-04-27 12:59:33 +05:30
Saqib Ansari
e4274c1cc5 fix: travis 2021-04-27 12:59:33 +05:30
Saqib Ansari
1e7133baf1 fix: sider 2021-04-27 12:59:08 +05:30
Saqib Ansari
2c56ae78a7 feat: default discount account in company 2021-04-27 12:59:07 +05:30
Saqib Ansari
666af17cfa refactor: discount on early payments 2021-04-27 12:58:32 +05:30
Saqib Ansari
88da5f9073 fix: translation syntax 2021-04-27 12:57:59 +05:30
Saqib Ansari
1dd3396b60 fix: condition 2021-04-27 12:57:19 +05:30
Saqib Ansari
153323ee41 refactor: remove discount validity date 2021-04-27 12:57:05 +05:30
Saqib Ansari
09ef7b62a1 feat: discount validity date in payment terms 2021-04-27 12:56:05 +05:30
Saqib Ansari
e2e2a305da feat: init outstanding amount same as payment amount 2021-04-27 12:55:34 +05:30
Saqib Ansari
c648027473 feat: early payment discount for purchase invoice 2021-04-27 12:54:44 +05:30
Saqib Ansari
14096fdfc8 chore: rename total amount to grand total 2021-04-27 12:54:43 +05:30
Saqib Ansari
4bc4e96c2a feat: update paid amount, discounted amount & outstanding 2021-04-27 12:54:43 +05:30
Saqib Ansari
b8d5db6689 feat: invoice references based on payment terms 2021-04-27 12:54:42 +05:30
Saqib Ansari
4b9aed2c0e feat: add outstanding field in payment schedule 2021-04-27 12:54:39 +05:30
Saqib Ansari
7a3dbe1b72 feat: update discounted amount in payment terms 2021-04-27 12:54:04 +05:30
Saqib Ansari
cf23f83d6c feat: discount on early payment in payment terms 2021-04-27 12:54:00 +05:30
Deepesh Garg
f0816acfc2 Merge branch 'version-13-beta' of https://github.com/frappe/erpnext into enconnex_erpnext 2021-04-27 12:30:47 +05:30
Saqib Ansari
d73f1a8170 feat: set dynamic labels for payment schedule fields 2021-04-27 10:00:00 +05:30
Saqib Ansari
9faaa8ffd8 feat: base payment amount in payment schedule 2021-04-27 09:58:43 +05:30
Saqib Ansari
afb60c3678 fix: payment amount showing in foreign currency 2021-04-27 09:58:29 +05:30
Saqib Ansari
542c959257 feat: set dynamic labels for payment schedule fields 2021-04-26 19:21:47 +05:30
Saqib Ansari
70d5d2cb2b feat: base payment amount in payment schedule 2021-04-26 19:20:54 +05:30
Saqib Ansari
c55bbc2db9 fix: payment amount showing in foreign currency 2021-04-26 19:18:00 +05:30
Deepesh Garg
f9c13bf32b Merge pull request #25347 from nextchamp-saqib/einvoice-company-validation
fix(e-invoicing): validation & tax calculation fixes
2021-04-22 13:02:55 +05:30
Saqib Ansari
03107ffedd fix: shipping address 2021-04-21 12:31:22 +05:30
Afshan
68c0427aef chores: clean up 2021-04-20 13:13:26 +05:30
Afshan
e6d9d47cb4 feat: Dimension-wise Accounts Balance Report 2021-04-20 13:13:10 +05:30
Deepesh Garg
afbe93de3c Merge branch 'version-13-beta' of https://github.com/frappe/erpnext into enconnex_erpnext 2021-04-20 13:02:49 +05:30
Saqib Ansari
62895a4ca5 fix: test 2021-04-17 15:41:32 +05:30
Deepesh Garg
868b816c5a fix: Variable scope issue 2021-04-16 15:51:45 +05:30
Saqib Ansari
66773cabef fix: test 2021-04-15 16:44:55 +05:30
Saqib Ansari
a44db68b8e fix: remove extra condition 2021-04-15 13:40:31 +05:30
Saqib Ansari
6b198241e1 fix: validate total condition 2021-04-15 13:36:11 +05:30
Saqib Ansari
a9f3c3e50a feat: add company link to e-invoice settings 2021-04-15 13:32:31 +05:30
Saqib Ansari
ba563ca232 fix: test 2021-04-15 13:28:05 +05:30
Saqib Ansari
103ce8bbb7 fix: imports 2021-04-15 13:27:57 +05:30
Saqib Ansari
93c67ffd92 fix: sider 2021-04-15 13:26:26 +05:30
Saqib Ansari
491987b8d3 fix: except einvoice loading error seperately 2021-04-15 13:26:20 +05:30
Saqib Ansari
552330f744 fix: address validations & cancel eway bill dialog 2021-04-15 13:26:10 +05:30
Deepesh Garg
8e01c8aefb fix: GST on freight charge in e-invoicing 2021-04-15 13:22:39 +05:30
Saqib
ff57fbe2c1 fix(e-invoice): mandatory field pincode not found (#25200) 2021-04-05 14:33:01 +05:30
Saqib
fb0936be50 fix(india): critical e-invoicing fixes (#25164)
* fix: place of supply & same object reference issue

* fix(regional): remove shipping address GSTIN validation for e-invoice

* fix: stopiteration error

Co-authored-by: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com>
2021-04-05 11:00:37 +05:30
Deepesh Garg
ed369c105f fix: Role to override maintain same rate check in transactions 2021-04-04 21:31:11 +05:30
Rohit Waghchaure
192002e156 feat: purchase receipt creation from purchase invoice 2021-04-04 18:23:20 +05:30
Rucha Mahabal
52ac8484ff feat: configurable action if the same purchase/selling rate is not maintained 2021-03-15 16:20:19 +05:30
Deepesh Garg
bcb7848c84 fix: Add test for integration item sync 2021-03-15 10:45:38 +05:30
Deepesh Garg
19ca4d000b fix: Consider item integration mapping in Shopify 2021-03-15 10:45:27 +05:30
Deepesh Garg
288082abfd feat: Sync common items in ERPNext Integrations 2021-03-15 10:45:15 +05:30
Deepesh Garg
2b1809bd0e Merge branch 'version-13-beta' of https://github.com/frappe/erpnext into enconnex_erpnext 2021-03-13 23:18:13 +05:30
Deepesh Garg
5fab55a564 fix: Always use https scheme 2021-03-12 12:51:33 +05:30
Deepesh Garg
799f0daa1e fix: Use https protocol for site URL 2021-03-11 21:41:46 +05:30
Deepesh Garg
2f4b91172d feat: Role to allow over billing, delivery, receipt 2021-03-11 20:47:13 +05:30
Nabin Hait
adb4225fa7 fix: reposting patch fixes (#24775) 2021-03-11 12:28:02 +05:30
Nabin Hait
fdcc926597 fix: rounding of earned leave is optional 2021-03-11 12:25:06 +05:30
Anupam
04fa2e8d29 feat: provistion to pull timesheet in sales invoice 2021-03-08 17:03:32 +05:30
Deepesh Garg
0c6d69e27c fix: Add search field in project query 2021-02-25 14:11:49 +05:30
Rucha Mahabal
12c957b2b2 feat: add descriptions for YTD and MTD fields 2021-02-19 21:01:56 +05:30
Rucha Mahabal
11cef55649 test: year to date computation for salary slip components 2021-02-19 21:00:54 +05:30
Rucha Mahabal
1585986fa4 feat: Salary Slip with Year to Date Print Format 2021-02-19 21:00:18 +05:30
Rucha Mahabal
10c069588f feat(Payroll): compute Year to Date for Salary Slip components 2021-02-19 21:00:08 +05:30
Saqib Ansari
ca966601a2 fix: tcs chargable amount 2021-02-19 20:33:24 +05:30
Deepesh Garg
655a57513f feat: Additon of leave details in Salary Slip 2021-02-19 20:32:27 +05:30
Saqib Ansari
a4fabefee7 fix: sider 2021-02-17 13:06:31 +05:30
Saqib Ansari
5bb15b08d9 fix: tcs amount calculation 2021-02-17 13:06:11 +05:30
Saqib Ansari
f045eb3135 fix: tcs chargable amount 2021-02-17 13:05:54 +05:30
Saqib Ansari
62890adef4 feat: charging tcs on sales invoice 2021-02-17 13:05:24 +05:30
Saqib Ansari
f000b3b362 fix: test 2021-02-17 13:04:27 +05:30
Saqib Ansari
1a8df8f112 feat: pan and tax withholding category fields for customer 2021-02-17 13:04:27 +05:30
Saqib Ansari
760e6cebd4 refactor: tax withholding category against customer 2021-02-17 13:04:27 +05:30
Deepesh Garg
7835ec6a6c fix: Add test case 2021-02-01 12:15:39 +05:30
Deepesh Garg
5c7333bd3a feat: Normal rounding for GST Taxes 2021-02-01 12:15:28 +05:30
Deepesh Garg
a821647283 fix(india): GST Taxes for intra state transfer 2021-01-25 13:56:25 +05:30
Deepesh Garg
f6ffdcef98 fix(india): GST Taxes for intra state transfer 2021-01-25 13:56:09 +05:30
Deepesh Garg
71976de79c fix: Internal Party validation fix 2021-01-21 18:25:24 +05:30
Deepesh Garg
3f6f20b0d2 fix: Purchase Order and Invoice linking 2021-01-21 16:00:17 +05:30
Deepesh Garg
d9a6a6fe6a fix: Tax updation for internal invoices 2021-01-20 17:35:33 +05:30
Deepesh Garg
1cfb69aaae fix: Test Cases 2021-01-20 10:27:01 +05:30
Deepesh Garg
50cd7a1650 fix: Import Error 2021-01-19 19:35:13 +05:30
Deepesh Garg
f4a4efb510 fix: address mapping between sales and purchase document 2021-01-19 19:05:50 +05:30
Deepesh Garg
7337669e49 feta: Provision to disbale serial no and batch selector 2021-01-18 15:21:05 +05:30
Deepesh Garg
fc34ded7a8 fix: Ref doc map 2021-01-15 14:12:57 +05:30
Deepesh Garg
342299fe3b fix: Update timestamp 2021-01-15 10:14:18 +05:30
Deepesh Garg
5307f8e794 Revert "fix: Move item price link to batch"
This reverts commit 8963edae51.
2021-01-15 10:12:22 +05:30
Deepesh Garg
540c119a27 Revert "fix: Sider issues"
This reverts commit d3fbefcdf7.
2021-01-15 10:11:27 +05:30
Deepesh Garg
21479fc5ea Revert "fix: Add missing semicolon"
This reverts commit a5539dc580.
2021-01-15 10:11:15 +05:30
Deepesh Garg
2c12124550 Revert "fix: Item Price using batch selector"
This reverts commit 34c58529d9.
2021-01-15 10:10:54 +05:30
Deepesh Garg
f68e85519c Revert "fix: Price not getting set for first row"
This reverts commit 92ed11ad16.
2021-01-15 10:05:58 +05:30
Deepesh Garg
0868bf5d45 Revert "fix: Set batch values"
This reverts commit 0bbb55d6cc.
2021-01-15 10:05:37 +05:30
Deepesh Garg
8bc69d3fe0 Revert "fix: Minor Issues"
This reverts commit 1907c8f30b.
2021-01-15 10:05:21 +05:30
Deepesh Garg
cd3d6ade04 fix: Add shipping_address to no field map 2021-01-14 14:35:54 +05:30
Deepesh Garg
9070ec3237 fix: GST Purchasse register and other fixes 2021-01-14 14:07:10 +05:30
Deepesh Garg
cfcb1a15f8 fix: remove self 2021-01-13 19:33:07 +05:30
Deepesh Garg
d0ee3fc0cd fix: Target field in Sales Invoice 2021-01-13 19:23:36 +05:30
Deepesh Garg
776d89c4de fix: From warehouse in Purchase Order 2021-01-13 19:04:53 +05:30
Deepesh Garg
a02cad5ec4 fix: Item wise gst sales register fix 2021-01-13 16:44:37 +05:30
Deepesh Garg
837aae7bcb fix: Unreallized profit in Sales Register 2021-01-13 16:43:31 +05:30
Deepesh Garg
cfcc9eac39 fix: Rate forcing in sales order 2021-01-11 18:20:23 +05:30
Deepesh Garg
4b86f0440e fix: Remove commented code 2021-01-11 18:20:15 +05:30
Deepesh Garg
f709d57475 fix: Check for custom dimensions 2021-01-05 15:55:19 +05:30
Deepesh Garg
377fe373e2 fix: Minor Issues 2020-12-31 11:05:11 +05:30
Deepesh Garg
39ba31e68f fix: Set batch values 2020-12-28 23:35:03 +05:30
Deepesh Garg
6d3e843f07 fix: Price not getting set for first row 2020-12-28 23:22:09 +05:30
Deepesh Garg
a679d43722 fix: Item Price using batch selector 2020-12-28 21:14:13 +05:30
Deepesh Garg
2466c3214a fix: Add missing semicolon 2020-12-28 21:13:54 +05:30
Deepesh Garg
646ead65d8 fix: Sider issues 2020-12-28 21:13:43 +05:30
Deepesh Garg
7b282d253f fix: Move item price link to batch 2020-12-28 21:13:36 +05:30
Deepesh Garg
37320074b2 fix: Sider Issues 2020-12-28 21:13:27 +05:30
Deepesh Garg
a5353c7d2d fix: Add test case for batch pricing 2020-12-28 21:13:17 +05:30
Deepesh Garg
e85050c190 fix: Rate for items with no batch 2020-12-28 21:13:09 +05:30
Deepesh Garg
c3b22c6e5c feat: Batch wise item pricing 2020-12-28 21:12:58 +05:30
Deepesh Garg
73cef56df7 fix: Debug travis 2020-12-28 17:44:00 +05:30
Deepesh Garg
a0f6381d6e fix: Debugging 2020-12-28 17:43:21 +05:30
Deepesh Garg
b7b3944cef fix: Internal sales item link in Purchase Invoice 2020-12-28 17:43:11 +05:30
Deepesh Garg
9d4bf040e9 fix: Fixes in flow 2020-12-28 17:11:10 +05:30
Deepesh Garg
afef92a88b fix: Add validations and other fixes 2020-12-28 17:10:53 +05:30
Deepesh Garg
a9f4508cd1 fix: Add missing set warehouse fields 2020-12-28 17:06:14 +05:30
Deepesh Garg
dac19cbca5 fix: Consider conversion factor for invoices 2020-12-28 17:05:25 +05:30
Deepesh Garg
573d9094bb fix: Item valuation for internal stocktransfers 2020-12-28 17:05:16 +05:30
Deepesh Garg
0413e34f0d Merge branch 'version-13-beta-pre-release' of https://github.com/frappe/erpnext into enconnex_erpnext 2020-12-28 17:04:35 +05:30
Deepesh Garg
7d2c5c24c6 Merge branch 'enconnex_erpnext' of https://github.com/frappe/erpnext into enconnex_erpnext 2020-12-23 10:39:56 +05:30
Deepesh Garg
107e2cdaba fix: Tax template update on supplier 2020-12-23 10:37:55 +05:30
Deepesh Garg
a2bf727794 fix: Tax template update on customer address change 2020-12-23 10:37:08 +05:30
Deepesh Garg
ed93d41727 Merge branch 'enconnex_erpnext' of https://github.com/frappe/erpnext into enconnex_erpnext 2020-12-14 09:19:42 +05:30
Deepesh Garg
7b458be170 fix: Map warehouse and serial no 2020-12-14 09:16:42 +05:30
Deepesh Garg
9d806df589 fix: Commonfied code 2020-12-14 09:15:48 +05:30
Deepesh Garg
dd2f6aade1 fix: Add description for fields 2020-12-14 09:14:44 +05:30
Deepesh Garg
0f30c52759 fix: Test Case 2020-12-14 09:12:30 +05:30
Deepesh Garg
610eb508bf fix: Add test for internal transfer 2020-12-04 11:32:28 +05:30
Deepesh Garg
047bf3833e fix: Account naming changes and other fixes 2020-12-04 11:32:19 +05:30
Deepesh Garg
e4554f690c fix: Dimension filters in accounting reports 2020-12-04 11:31:41 +05:30
Deepesh Garg
57d79a8149 fix: GL entry fixes and validation for intercompany account 2020-12-02 20:51:36 +05:30
Deepesh Garg
65fb0729d2 fix: Linting issues 2020-12-02 20:49:05 +05:30
Deepesh Garg
692c617cb2 fix: warehouse fetching 2020-12-02 20:48:50 +05:30
Deepesh Garg
614c04e357 fix: Accounting for internal transfer invoices within same company 2020-12-02 20:48:36 +05:30
Deepesh Garg
07e86e0c3d fix: Move filter setup to doctype controllers 2020-12-02 13:54:26 +05:30
Deepesh Garg
da9418b378 fix: Exception naming 2020-12-02 13:05:20 +05:30
Deepesh Garg
8e641d353c fix: Accounting dimension import in PCV 2020-12-02 13:05:08 +05:30
Deepesh Garg
e00f52c455 fix: Move filter setup to doctype controllers 2020-12-01 21:11:19 +05:30
Deepesh Garg
c4bdbe1046 fix: Label changes 2020-12-01 21:10:32 +05:30
Deepesh Garg
4d6c53f7b2 fix: Define specific exceptions and fix tests 2020-12-01 21:10:21 +05:30
Deepesh Garg
94aad17fd5 fix: form layout and naming fixes 2020-12-01 21:10:10 +05:30
Deepesh Garg
08d562a5bb fix: Replace raw query with ORM 2020-12-01 21:09:53 +05:30
Deepesh Garg
509e40a584 fix: Account filters 2020-11-17 13:52:46 +05:30
Deepesh Garg
cb80aaf707 fix: Disable filters after test 2020-11-17 13:52:38 +05:30
Deepesh Garg
0ce888ec08 fix: Linting and test cases 2020-11-17 13:16:44 +05:30
Deepesh Garg
4569c9b5e3 fix: Add test case 2020-11-17 13:16:36 +05:30
Deepesh Garg
e895ddc952 fix: Add disable and mandatory check for accounting dimension filters 2020-11-17 13:16:25 +05:30
Deepesh Garg
3a02fd9bda fix: GL Entry validation for dimensions 2020-11-17 13:16:14 +05:30
Deepesh Garg
fb4ad9042c fix: linting 2020-11-17 13:16:04 +05:30
Deepesh Garg
d2b44e90d6 fix: Remove project filter 2020-11-17 13:15:51 +05:30
Deepesh Garg
094328b8c9 fix: Remove cost center query from doctypes 2020-11-17 13:13:09 +05:30
Deepesh Garg
317a0495e2 fix: dimension filter query 2020-11-17 13:12:59 +05:30
Deepesh Garg
e3f9c5246e fix: dimension filter query 2020-11-17 13:12:50 +05:30
Deepesh Garg
c56d775541 feat: Add accounting dimension filter doctype 2020-11-17 13:12:40 +05:30
Deepesh Garg
75037e4493 fix: Remarks for penalty GL Entry 2020-11-10 17:54:51 +05:30
Deepesh Garg
465fc5163f fix: Party for loan ledger entries 2020-11-09 18:38:53 +05:30
Deepesh Garg
9c88c37102 fix: Loan write off precision issue 2020-11-07 17:10:34 +05:30
Deepesh Garg
f3bf9aa70f fix: Remarks fix 2020-11-07 00:15:49 +05:30
Deepesh Garg
9765809264 fix: Loan seurity unpledge msg improvement 2020-11-06 17:43:45 +05:30
Deepesh Garg
0803102a92 fix: Add better remarks for Loan GL entries 2020-11-06 14:29:32 +05:30
Deepesh Garg
34650b7122 fix: Remove accrual type from process 2020-11-05 21:24:09 +05:30
Deepesh Garg
28ec74d35e fix: Penalty amount calculation fix 2020-11-05 21:23:29 +05:30
Deepesh Garg
fc45f9dc17 fix: Negative amount check for amounts 2020-11-05 21:23:15 +05:30
Deepesh Garg
8ade3b25ab fix: Interest accrual after loan topup 2020-10-28 13:41:41 +05:30
Deepesh Garg
dbb378f759 fix: Translation syntax 2020-10-26 19:03:38 +05:30
Deepesh Garg
62cd0c6592 fix: Loan disbursement amount validation check 2020-10-26 18:56:47 +05:30
Deepesh Garg
97dafc06f9 fix: Unaccrued interest from last accrual date instead of disbursement date 2020-10-26 14:25:21 +05:30
Deepesh Garg
dc98db611b fix: Cancel repayment accrual interest entry on payment cancellation 2020-10-23 19:04:15 +05:30
Deepesh Garg
bf5a791d0e fix: Unaccrued interest after disbursal 2020-10-22 21:53:28 +05:30
Saqib Ansari
b8b66a1450 chore: move e-invoice settings to regional 2020-10-22 09:42:32 +05:30
Saqib Ansari
e1b7d465c4 feat: complete e-invoice schema 2020-10-22 09:41:23 +05:30
Saqib Ansari
db2d221976 feat: cancel IRN 2020-10-22 09:41:23 +05:30
Saqib Ansari
383a3df807 chore: validations 2020-10-22 09:41:23 +05:30
Saqib Ansari
bc3b5e334b feat: decode signed json and QR code 2020-10-22 09:41:23 +05:30
Saqib Ansari
b855a32d84 feat: generate IRN 2020-10-22 09:41:23 +05:30
Saqib Ansari
8fe07e4567 feat: make e invoice from erpnext sales invoice 2020-10-22 09:41:23 +05:30
Saqib Ansari
da0c064c70 feat: decrypt json data with SEK 2020-10-22 09:41:23 +05:30
Saqib Ansari
f3d74b7638 feat: AES decryption of SEK with appkey 2020-10-22 09:41:23 +05:30
Saqib Ansari
529727224c chore: handle error response 2020-10-22 09:41:23 +05:30
Saqib Ansari
9546e3ea32 feat: save token and sek from auth request 2020-10-22 09:41:23 +05:30
Saqib Ansari
6f035d2169 feat: rsa encryption with public key 2020-10-22 09:41:23 +05:30
Saqib Ansari
8d612d3f78 feat: read public key file 2020-10-22 09:41:23 +05:30
Saqib Ansari
c1acb7b77a feat: init e-invoice settings 2020-10-22 09:41:23 +05:30
Deepesh Garg
e0ef4abbcf fix: Test Cases 2020-10-22 09:40:15 +05:30
Deepesh Garg
bd16e0885b fix: Add test for loan top up 2020-10-22 09:40:15 +05:30
Deepesh Garg
35f985e514 fix: Update no copy fields 2020-10-22 09:40:14 +05:30
Deepesh Garg
b32de3ab13 fix: Test Case 2020-10-22 09:40:14 +05:30
Deepesh Garg
4c420ca490 fix: Validatiion for loan write off amountt 2020-10-22 09:40:14 +05:30
Deepesh Garg
469c23b562 fix: Add write off test 2020-10-19 09:34:53 +05:30
Deepesh Garg
3fe127d483 fix: Write Off amount handling in Loan accrual and closure 2020-10-19 09:34:44 +05:30
Deepesh Garg
78b30ccc4f feat: Add loan write off doctype 2020-10-19 09:34:36 +05:30
Deepesh Garg
10fdd38e32 fix: Loan Security unpledge on loan cancel 2020-10-13 18:17:04 +05:30
Deepesh Garg
dc9438c3a4 fix: Add unaccrued interest in interest amount for loan closure 2020-10-13 10:00:35 +05:30
Deepesh Garg
6771d97c42 fix: Acrual type 2020-10-13 10:00:35 +05:30
Deepesh Garg
ec17111e27 fix: Remove repayment type 2020-10-13 10:00:35 +05:30
Deepesh Garg
8679ce475a fix: Add accrual type and penalty field in interest accrual 2020-10-13 10:00:35 +05:30
Deepesh Garg
fd3952ba9a fix: Button to close loan 2020-10-13 10:00:35 +05:30
Deepesh Garg
5138f00176 fix: Add method for loan closure 2020-10-13 10:00:35 +05:30
Deepesh Garg
6a24d2df8c Merge branch 'develop' of https://github.com/frappe/erpnext into enconnex_erpnext 2020-10-13 09:54:04 +05:30
Saqib Ansari
96bd2f72d0 fix: fetch token if not valid 2020-10-07 14:33:38 +05:30
Saqib Ansari
fb33f7d3c3 chore: save signed invoice and qrcode after uplaoding irn 2020-10-07 11:29:10 +05:30
Saqib Ansari
5e0817f57a fix: fn name 2020-10-07 11:29:10 +05:30
Saqib Ansari
f6d23df826 chore: group e-invoicing actions 2020-10-07 11:29:10 +05:30
Saqib Ansari
499273e64a feat: manual download / upload json 2020-10-07 11:29:09 +05:30
Saqib Ansari
14c3715fef feat: cancel e-way bill before cancelling IRN 2020-10-01 09:22:04 +05:30
Saqib Ansari
d43d7a0a58 chore: no copy on e invoice custom fields 2020-10-01 09:22:04 +05:30
Saqib Ansari
67e68670bb fix: save e-way bill no on irn generation 2020-10-01 09:22:04 +05:30
Saqib Ansari
9dcab60d61 feat: e-way bill details in e-invoice 2020-10-01 09:22:04 +05:30
Saqib Ansari
30a7da51d3 chore: show irn field for proper gst_category 2020-10-01 09:22:04 +05:30
Saqib Ansari
df859fdc23 fix: update irn_cancelled after cancelling irn 2020-10-01 09:22:04 +05:30
Saqib Ansari
48527c48d2 fix: do not show generate irn for invalid supply type 2020-10-01 09:22:04 +05:30
Saqib Ansari
df09c76148 fix: validation if e invoicing is disabled 2020-10-01 09:22:04 +05:30
Saqib Ansari
0ce26ce90d fix: cannot find attached key file 2020-10-01 09:22:04 +05:30
Saqib Ansari
a9f65654a1 fix: public key is required on validate 2020-10-01 09:22:04 +05:30
Saqib Ansari
57caf99a7e fix: hide cancel irn dialog on error 2020-09-30 13:18:37 +05:30
Saqib Ansari
d729f7f50e chore: show irn cancelled check after cancellation 2020-09-30 13:18:37 +05:30
Saqib Ansari
6fce734476 fix: item discount 2020-09-30 13:18:37 +05:30
Saqib Ansari
354da7ad4c chore: minor fixes 2020-09-30 13:18:37 +05:30
Saqib Ansari
aaf445af98 feat: Generate & Cancel IRN from Sales Invoice 2020-09-30 13:18:37 +05:30
Saqib Ansari
6eb4fe6659 feat: make IRN field on regional setup 2020-09-30 13:18:37 +05:30
Saqib Ansari
3739dc3f78 chore: rename schema to template & js cleanup 2020-09-30 13:18:37 +05:30
Saqib Ansari
dc3c03e47e chore: split einvoice settings and operations 2020-09-30 13:18:37 +05:30
Saqib Ansari
2fbc8c8f30 chore: move e-invoice settings to regional 2020-09-30 13:18:37 +05:30
Saqib Ansari
06a21da29e feat: complete e-invoice schema 2020-09-30 13:18:37 +05:30
Saqib Ansari
b488872cbd feat: cancel IRN 2020-09-30 13:18:37 +05:30
Saqib Ansari
88fa9866b1 chore: validations 2020-09-30 13:18:37 +05:30
Saqib Ansari
62109c3c1a feat: decode signed json and QR code 2020-09-30 13:18:37 +05:30
Saqib Ansari
1bbdc504bf feat: generate IRN 2020-09-30 13:18:37 +05:30
Saqib Ansari
e43a50357b feat: make e invoice from erpnext sales invoice 2020-09-30 13:18:37 +05:30
Saqib Ansari
17cd44ecd7 feat: decrypt json data with SEK 2020-09-30 13:18:36 +05:30
Saqib Ansari
54ebb06d8a feat: AES decryption of SEK with appkey 2020-09-30 13:18:36 +05:30
Saqib Ansari
ddf9d9ae0e chore: handle error response 2020-09-30 13:18:36 +05:30
Saqib Ansari
5488ccfa8c feat: save token and sek from auth request 2020-09-30 13:18:36 +05:30
Saqib Ansari
0b304b3561 feat: rsa encryption with public key 2020-09-30 13:18:36 +05:30
Saqib Ansari
4971061054 feat: read public key file 2020-09-30 13:18:36 +05:30
Saqib Ansari
a7167f775d feat: init e-invoice settings 2020-09-30 13:18:36 +05:30
Deepesh Garg
1ccba26173 Merge branch 'version-13-beta' of https://github.com/frappe/erpnext into enconnex_erpnext 2020-09-30 12:31:19 +05:30
Deepesh Garg
ffbf30809d fix: filter pricing rules based on condition 2020-08-14 22:37:27 +05:30
Deepesh Garg
ad02cfe98e Merge branch version-13-beta into enconnex_erpnext 2020-08-14 19:21:54 +05:30
Abhishek Balam
392a2856d3 fix: condition syntax validation readded, fetch item details if condition not met ignoring rule 2020-08-14 19:15:06 +05:30
Abhishek Balam
56b0136696 fix: eval fail message fix 2020-08-13 12:54:38 +05:30
Deepesh Garg
da82a380e1 fix: Condition fix 2020-08-13 12:54:30 +05:30
Deepesh Garg
c0e3bcf865 fix: Pass doc instead of args 2020-08-13 12:54:23 +05:30
Abhishek Balam
398c0f0b7f feat: add condition field in pricing rule 2020-08-13 12:51:35 +05:30
Abhishek Balam
6b9f7e35f4 fix: change opportunity to 'Converted' when items not selected in opportunity itself for making quotation and sales order 2020-08-11 16:17:34 +05:30
Deepesh Garg
f9a6a90900 fix: Ignore cpmpany and bank account doctype while deleting company transactions 2020-08-10 18:26:47 +05:30
Deepesh Garg
9ebe8d5895 fix: Add default billing address for purchase documents 2020-08-07 19:49:08 +05:30
marination
24c5fc862d fix: Employee benefit application & quality inspection whitelist 2020-07-20 16:57:16 +05:30
Chinmay D. Pai
0c92e7f9a1 chore: add query functions to whitelist
Signed-off-by: Chinmay D. Pai <chinmaydpai@gmail.com>
2020-07-20 16:57:00 +05:30
Chinmay D. Pai
c788ee28c2 fix: whitelist all query functions for search widget
Signed-off-by: Chinmay D. Pai <chinmaydpai@gmail.com>
2020-07-20 16:56:52 +05:30
92 changed files with 6596 additions and 2021 deletions

View File

@@ -109,7 +109,7 @@ def get_region(company=None):
'''
if company or frappe.flags.company:
return frappe.get_cached_value('Company',
company or frappe.flags.company, 'country')
company or frappe.flags.company, 'country')
elif frappe.flags.country:
return frappe.flags.country
else:

View File

@@ -12,6 +12,7 @@
"frozen_accounts_modifier",
"determine_address_tax_category_from",
"over_billing_allowance",
"role_allowed_to_over_bill",
"column_break_4",
"credit_controller",
"check_supplier_invoice_uniqueness",
@@ -226,6 +227,13 @@
"fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
},
{
"description": "Users with this role are allowed to over bill above the allowance perecentage",
"fieldname": "role_allowed_to_over_bill",
"fieldtype": "Link",
"label": "Role Allowed to Over Bill ",
"options": "Role"
}
],
"icon": "icon-cog",
@@ -233,7 +241,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-05 13:04:00.118892",
"modified": "2021-03-11 18:52:05.601996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -1,196 +1,82 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-01-02 15:48:58.768352",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-01-02 15:48:58.768352",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"cgst_account",
"sgst_account",
"igst_account",
"cess_account",
"is_reverse_charge_account"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 1,
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cgst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "CGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "cgst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "CGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sgst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "SGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "sgst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "SGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "igst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "IGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "igst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "IGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cess_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "CESS Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"columns": 2,
"fieldname": "cess_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "CESS Account",
"options": "Account"
},
{
"columns": 1,
"default": "0",
"fieldname": "is_reverse_charge_account",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Reverse Charge Account"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-02 15:52:22.335988",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-09 12:30:25.889993",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -283,7 +283,9 @@ frappe.ui.form.on('Payment Entry', {
let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", "");
frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
frappe.throw(
__("Party can only be one of {0}", [party_types.join(", ")])
);
}
frm.set_query("party", function() {
@@ -637,13 +639,13 @@ frappe.ui.form.on('Payment Entry', {
let to_field = fields[key][1];
if (filters[from_field] && !filters[to_field]) {
frappe.throw(__("Error: {0} is mandatory field",
[to_field.replace(/_/g, " ")]
));
frappe.throw(
__("Error: {0} is mandatory field", [to_field.replace(/_/g, " ")])
);
} else if (filters[from_field] && filters[from_field] > filters[to_field]) {
frappe.throw(__("{0}: {1} must be less than {2}",
[key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")]
));
frappe.throw(
__("{0}: {1} must be less than {2}", [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")])
);
}
}
},
@@ -692,6 +694,8 @@ frappe.ui.form.on('Payment Entry', {
c.total_amount = d.invoice_amount;
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
c.payment_term = d.payment_term;
c.allocated_amount = d.allocated_amount;
if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) {
if(flt(d.outstanding_amount) > 0)
@@ -774,12 +778,15 @@ frappe.ui.form.on('Payment Entry', {
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) {
frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice",
[frm.doc.payment_type,
(frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type]));
frappe.msgprint(
__("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
(frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
);
return false
} else {
frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding]));
frappe.msgprint(
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
);
return false;
}
} else {
@@ -791,10 +798,13 @@ frappe.ui.form.on('Payment Entry', {
}
$.each(frm.doc.references || [], function(i, row) {
row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount
if(frappe.flags.allocate_payment_amount != 0){
if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if(row.outstanding_amount >= allocated_positive_outstanding) {
if (frappe.flags.allocate_payment_amount == 0) {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
@@ -802,9 +812,11 @@ frappe.ui.form.on('Payment Entry', {
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding)
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
else row.allocated_amount = row.outstanding_amount;
} else {
row.allocated_amount = row.outstanding_amount;
};
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}

View File

@@ -333,33 +333,50 @@ class PaymentEntry(AccountsController):
invoice_payment_amount_map = {}
invoice_paid_amount_map = {}
for reference in self.get('references'):
if reference.payment_term and reference.reference_name:
key = (reference.payment_term, reference.reference_name)
for ref in self.get('references'):
if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name)
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += reference.allocated_amount
invoice_payment_amount_map[key] += ref.allocated_amount
if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name},
fields=['paid_amount', 'payment_amount', 'payment_term'])
payment_schedule = frappe.get_all(
'Payment Schedule',
filters={'parent': ref.reference_name},
fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding']
)
for term in payment_schedule:
invoice_key = (term.payment_term, reference.reference_name)
invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
for key, allocated_amount in iteritems(invoice_payment_amount_map):
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
for key, amount in iteritems(invoice_payment_amount_map):
if cancel:
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
frappe.db.sql("""
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
else:
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
if amount > outstanding:
if allocated_amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
if amount and outstanding:
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
if allocated_amount and outstanding:
frappe.db.sql("""
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
def set_status(self):
if self.docstatus == 2:
@@ -704,6 +721,8 @@ def get_outstanding_reference_documents(args):
outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
args.get("party_account"), filters=args, condition=condition)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
for d in outstanding_invoices:
d["exchange_rate"] = 1
if party_account_currency != company_currency:
@@ -731,6 +750,46 @@ def get_outstanding_reference_documents(args):
return data
def split_invoices_based_on_payment_terms(outstanding_invoices):
invoice_ref_based_on_payment_terms = {}
for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']:
payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template')
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_value(
'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms')
if allocate_payment_based_on_payment_terms:
payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"])
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append(frappe._dict({
'due_date': d.due_date,
'currency': d.currency,
'voucher_no': d.voucher_no,
'voucher_type': d.voucher_type,
'posting_date': d.posting_date,
'invoice_amount': flt(d.invoice_amount),
'outstanding_amount': flt(d.outstanding_amount),
'payment_amount': payment_term.payment_amount,
'payment_term': payment_term.payment_term,
'allocated_amount': payment_term.outstanding
}))
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = outstanding_invoices[idx]['voucher_no']
voucher_type = outstanding_invoices[idx]['voucher_type']
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
voucher_type, voucher_no, len(ref)), alert=True)
outstanding_invoices.pop(idx - 1)
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
return outstanding_invoices
def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None):
if party_type == "Customer":
@@ -1083,6 +1142,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc)
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
pe.company = doc.company
@@ -1152,11 +1213,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.setup_party_account_field()
pe.set_missing_values()
if party_account and bank:
if dt == "Employee Advance":
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
if discount_amount:
pe.set_gain_or_loss(account_details={
'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"),
'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"),
'amount': discount_amount * (-1 if payment_type == "Pay" else 1)
})
pe.set_difference_amount()
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1272,6 +1342,33 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':
discount_amount = flt(doc.get('grand_total')) * (term.discount / 100)
else:
discount_amount = term.discount
discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
if doc.doctype == 'Sales Invoice':
paid_amount -= discount_amount
received_amount -= discount_amount_in_foreign_currency
else:
received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency
total_discount += discount_amount
if total_discount:
money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency'))
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = []
for payment_term in payment_schedule:

View File

@@ -193,6 +193,34 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
si.payment_terms_template = 'Test Discount Template'
frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC')
si.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18
})
si.save()
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.submit()
si.load_from_db()
self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount')
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
@@ -591,6 +619,26 @@ def create_payment_terms_template():
}]
}).insert()
def create_payment_terms_template_with_discount():
create_payment_term('30 Credit Days with 10% Discount')
if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
payment_term_template = frappe.get_doc({
'doctype': 'Payment Terms Template',
'template_name': 'Test Discount Template',
'allocate_payment_based_on_payment_terms': 1,
'terms': [{
'doctype': 'Payment Terms Template Detail',
'payment_term': '30 Credit Days with 10% Discount',
'invoice_portion': 100,
'credit_days_based_on': 'Day(s) after invoice date',
'credit_days': 2,
'discount': 10,
'discount_validity_based_on': 'Day(s) after invoice date',
'discount_validity': 1
}]
}).insert()
def create_payment_term(name):
if not frappe.db.exists('Payment Term', name):

View File

@@ -58,7 +58,7 @@
"fieldname": "total_amount",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Amount",
"label": "Grand Total",
"print_hide": 1,
"read_only": 1
},
@@ -92,9 +92,10 @@
"options": "Payment Term"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-03-13 12:07:19.362539",
"modified": "2021-02-10 11:25:47.144392",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -6,12 +6,25 @@
"engine": "InnoDB",
"field_order": [
"payment_term",
"section_break_15",
"description",
"section_break_4",
"due_date",
"invoice_portion",
"payment_amount",
"mode_of_payment",
"paid_amount"
"column_break_5",
"invoice_portion",
"section_break_6",
"discount_type",
"discount_date",
"column_break_9",
"discount",
"section_break_9",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"column_break_3",
"base_payment_amount"
],
"fields": [
{
@@ -25,6 +38,7 @@
},
{
"columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@@ -62,14 +76,90 @@
"options": "Mode of Payment"
},
{
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
"label": "Paid Amount",
"options": "currency"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "discounted_amount",
"fieldname": "discounted_amount",
"fieldtype": "Currency",
"label": "Discounted Amount",
"read_only": 1
},
{
"fetch_from": "payment_amount",
"fieldname": "outstanding",
"fieldtype": "Currency",
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "discount",
"fieldname": "discount_date",
"fieldtype": "Date",
"label": "Discount Date",
"mandatory_depends_on": "discount"
},
{
"default": "Percentage",
"fetch_from": "payment_term.discount_type",
"fieldname": "discount_type",
"fieldtype": "Select",
"label": "Discount Type",
"options": "Percentage\nAmount"
},
{
"fetch_from": "payment_term.discount",
"fieldname": "discount",
"fieldtype": "Float",
"label": "Discount"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "base_payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-03-13 17:58:24.729526",
"modified": "2021-04-28 05:41:35.084233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",

View File

@@ -1,2 +1,22 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Payment Term', {
onload(frm) {
frm.trigger('set_dynamic_description');
},
discount(frm) {
frm.trigger('set_dynamic_description');
},
discount_type(frm) {
frm.trigger('set_dynamic_description');
},
set_dynamic_description(frm) {
if (frm.doc.discount) {
let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
if (frm.doc.discount_type == 'Amount') {
description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]);
}
frm.set_df_property("discount", "description", description);
}
}
});

View File

@@ -1,386 +1,166 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:payment_term_name",
"beta": 0,
"creation": "2017-08-10 15:24:54.876365",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:payment_term_name",
"creation": "2017-08-10 15:24:54.876365",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term_name",
"invoice_portion",
"mode_of_payment",
"column_break_3",
"due_date_based_on",
"credit_days",
"credit_months",
"section_break_8",
"discount_type",
"discount",
"column_break_11",
"discount_validity_based_on",
"discount_validity",
"section_break_6",
"description"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_term_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payment Term Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"bold": 1,
"fieldname": "payment_term_name",
"fieldtype": "Data",
"label": "Payment Term Name",
"unique": 1
},
{
"description": "Provide the invoice portion in percent",
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "invoice_portion",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoice Portion",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"bold": 1,
"fieldname": "invoice_portion",
"fieldtype": "Float",
"label": "Invoice Portion (%)"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Mode of Payment",
"length": 0,
"no_copy": 0,
"options": "Mode of Payment",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Due Date Based On",
"length": 0,
"no_copy": 0,
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"bold": 1,
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
},
{
"description": "Give number of days according to prior selection",
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Days",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"bold": 1,
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"label": "Credit Days"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"bold": 1,
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"label": "Discount Settings"
},
{
"default": "Percentage",
"fieldname": "discount_type",
"fieldtype": "Select",
"label": "Discount Type",
"options": "Percentage\nAmount"
},
{
"fieldname": "discount",
"fieldtype": "Float",
"label": "Discount"
},
{
"default": "Day(s) after invoice date",
"depends_on": "discount",
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
},
{
"depends_on": "discount",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"mandatory_depends_on": "discount"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-10-14 10:47:32.830478",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
"name_case": "",
"owner": "Administrator",
],
"links": [],
"modified": "2021-02-15 20:30:56.256403",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -3,11 +3,6 @@
frappe.ui.form.on('Payment Terms Template', {
setup: function(frm) {
frm.add_fetch("payment_term", "description", "description");
frm.add_fetch("payment_term", "invoice_portion", "invoice_portion");
frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on");
frm.add_fetch("payment_term", "credit_days", "credit_days");
frm.add_fetch("payment_term", "credit_months", "credit_months");
frm.add_fetch("payment_term", "mode_of_payment", "mode_of_payment");
}
});

View File

@@ -13,7 +13,6 @@ from frappe import _
class PaymentTermsTemplate(Document):
def validate(self):
self.validate_invoice_portion()
self.validate_credit_days()
self.check_duplicate_terms()
def validate_invoice_portion(self):
@@ -24,11 +23,6 @@ class PaymentTermsTemplate(Document):
if flt(total_portion, 2) != 100.00:
frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
def validate_credit_days(self):
for term in self.terms:
if cint(term.credit_days) < 0:
frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red')
def check_duplicate_terms(self):
terms = []
for term in self.terms:

View File

@@ -1,278 +1,164 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-08-10 15:34:09.409562",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2017-08-10 15:34:09.409562",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term",
"section_break_13",
"description",
"section_break_4",
"invoice_portion",
"mode_of_payment",
"column_break_3",
"due_date_based_on",
"credit_days",
"credit_months",
"section_break_8",
"discount_type",
"discount",
"column_break_11",
"discount_validity_based_on",
"discount_validity"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "payment_term",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Term",
"length": 0,
"no_copy": 0,
"options": "Payment Term",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "payment_term",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Term",
"options": "Payment Term"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"default": "0",
"fieldname": "invoice_portion",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Invoice Portion",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fetch_from": "payment_term.invoice_portion",
"fetch_if_empty": 1,
"fieldname": "invoice_portion",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Invoice Portion (%)",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Due Date Based On",
"length": 0,
"no_copy": 0,
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fetch_from": "payment_term.due_date_based_on",
"fetch_if_empty": 1,
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Due Date Based On",
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"default": "0",
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Credit Days",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"default": "0",
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fetch_from": "payment_term.credit_days",
"fetch_if_empty": 1,
"fieldname": "credit_days",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Credit Days",
"non_negative": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "0",
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fetch_from": "payment_term.credit_months",
"fetch_if_empty": 1,
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Mode of Payment",
"length": 0,
"no_copy": 0,
"options": "Mode of Payment",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fetch_from": "payment_term.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"label": "Discount Settings"
},
{
"default": "Percentage",
"fetch_from": "payment_term.discount_type",
"fetch_if_empty": 1,
"fieldname": "discount_type",
"fieldtype": "Select",
"label": "Discount Type",
"options": "Percentage\nAmount"
},
{
"fetch_from": "payment_term.discount",
"fetch_if_empty": 1,
"fieldname": "discount",
"fieldtype": "Float",
"label": "Discount"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Day(s) after invoice date",
"depends_on": "discount",
"fetch_from": "payment_term.discount_validity_based_on",
"fetch_if_empty": 1,
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
},
{
"collapsible": 1,
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"label": "Description"
},
{
"depends_on": "discount",
"fetch_from": "payment_term.discount_validity",
"fetch_if_empty": 1,
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"mandatory_depends_on": "discount"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-08-21 16:15:55.143025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Terms Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-24 11:56:12.410807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Terms Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -62,11 +62,11 @@ class POSProfile(Document):
if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
invalid_modes = []
for d in self.payments:
account = frappe.db.get_value(
"Mode of Payment Account",
"Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
"default_account"
)

View File

@@ -13,6 +13,7 @@
"apply_on",
"price_or_product_discount",
"warehouse",
"condition",
"column_break_7",
"items",
"item_groups",

View File

@@ -523,6 +523,28 @@ frappe.ui.form.on("Purchase Invoice", {
}
},
refresh: function(frm) {
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
}
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frappe.route_options = {
'purchase_invoice': frm.doc.name
}
frappe.set_route("List", "Purchase Receipt", "List")
}, __('View'));
}
},
onload: function(frm) {
if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
@@ -548,5 +570,13 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) {
hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
},
make_purchase_receipt: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
}
})

View File

@@ -164,7 +164,8 @@
"to_date",
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"per_received"
],
"fields": [
{
@@ -1372,13 +1373,22 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "per_received",
"fieldtype": "Percent",
"hidden": 1,
"label": "Per Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-03-09 21:56:28.748582",
"modified": "2021-03-28 04:13:59.829389",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -102,6 +102,7 @@ class PurchaseInvoice(BuyingController):
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
self.set_inter_company_account()
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@@ -376,6 +377,33 @@ class PurchaseInvoice(BuyingController):
where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)"""
})
def set_inter_company_account(self):
"""
Set intercompany account for inter warehouse transactions
This account will be used in case billing company and internal customer's
representation company is same
"""
if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account')
if not unrealized_profit_loss_account:
msg = _("Please select inter-company account or add default inter-company account for company {0}").format(
frappe.bold(self.company))
frappe.throw(msg)
self.unrealized_profit_loss_account = unrealized_profit_loss_account
def is_internal_transfer(self):
"""
It will an internal transfer if its an internal supplier and representation
company is same as billing company
"""
if self.is_internal_supplier and (self.represents_company == self.company):
return True
return False
def validate_purchase_receipt_if_update_stock(self):
if self.update_stock:
for item in self.get("items"):
@@ -1207,3 +1235,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
flt(obj.rate) * flt(source_parent.conversion_rate)
doc = get_mapped_doc("Purchase Invoice", source_name, {
"Purchase Invoice": {
"doctype": "Purchase Receipt",
"validation": {
"docstatus": ["=", 1],
}
},
"Purchase Invoice Item": {
"doctype": "Purchase Receipt Item",
"field_map": {
"name": "purchase_invoice_item",
"parent": "purchase_invoice",
"bom": "bom",
"purchase_order": "purchase_order",
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item"
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc

View File

@@ -397,7 +397,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.update({
"payment_schedule": get_payment_terms("_Test Payment Term Template",
pi.posting_date, pi.grand_total)
pi.posting_date, pi.grand_total, pi.base_grand_total)
})
pi.save()

View File

@@ -601,6 +601,7 @@
"oldfieldname": "purchase_order",
"oldfieldtype": "Link",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
@@ -804,7 +805,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-01-30 21:43:21.488258",
"modified": "2021-03-30 09:02:39.256602",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -585,6 +585,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("adjustment_against", function() {
return {
filters: {
company: frm.doc.company,
customer: frm.doc.customer,
docstatus: 1
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',
@@ -695,6 +705,7 @@ frappe.ui.form.on('Sales Invoice', {
refresh_field(['timesheets'])
}
})
frm.refresh();
},
onload: function(frm) {
@@ -810,6 +821,69 @@ frappe.ui.form.on('Sales Invoice', {
},
refresh: function(frm) {
if (frm.doc.project) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
"label" : "From",
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
},
{
fieldtype: 'Column Break',
fieldname: 'col_break_1',
},
{
"label" : "To",
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
}
],
primary_action: function() {
let data = d.get_values();
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
args: {
from_time: data.from_time,
to_time: data.to_time,
project: frm.doc.project
},
callback: function(r) {
if(!r.exc) {
if(r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amt,
'timesheet_detail': d.name
});
});
frm.refresh_field('timesheets')
}
else {
frappe.msgprint(__('No Timesheet Found.'))
}
d.hide();
}
}
});
},
primary_action_label: __('Get Timesheets')
});
d.show();
})
}
if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against');
}
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);

View File

@@ -17,6 +17,8 @@
"pos_profile",
"offline_pos_name",
"is_return",
"is_debit_note",
"update_billed_amount_in_sales_order",
"column_break1",
"company",
"company_tax_id",
@@ -405,15 +407,7 @@
"read_only": 1
},
{
"depends_on": "return_against",
"fieldname": "returns",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Returns"
},
{
"depends_on": "return_against",
"depends_on": "eval:doc.return_against || doc.is_debit_note",
"fieldname": "return_against",
"fieldtype": "Link",
"hide_days": 1,
@@ -422,7 +416,7 @@
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
"read_only": 1,
"read_only_depends_on": "eval:doc.is_return",
"search_index": 1
},
{
@@ -1982,6 +1976,12 @@
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "is_debit_note",
"fieldtype": "Check",
"label": "Is Debit Note"
}
],
"icon": "fa fa-file-text",
@@ -1995,7 +1995,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-01-12 12:16:15.192520",
"modified": "2021-04-22 22:36:32.916354",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -23,6 +23,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -76,6 +77,8 @@ class SalesInvoice(SellingController):
if not self.is_pos:
self.so_dn_required()
self.set_tax_withholding()
self.validate_proj_cust()
self.validate_pos_return()
self.validate_with_previous_doc()
@@ -91,6 +94,7 @@ class SalesInvoice(SellingController):
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
self.set_inter_company_account()
if cint(self.is_pos):
self.validate_pos()
@@ -153,6 +157,32 @@ class SalesInvoice(SellingController):
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
def set_tax_withholding(self):
tax_withholding_details = get_party_tax_withholding_details(self)
if not tax_withholding_details:
return
accounts = []
tax_withholding_account = tax_withholding_details.get("account_head")
for d in self.taxes:
if d.account_head == tax_withholding_account:
d.update(tax_withholding_details)
accounts.append(d.account_head)
if not accounts or tax_withholding_account not in accounts:
self.append("taxes", tax_withholding_details)
to_remove = [d for d in self.taxes
if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account]
for d in to_remove:
self.remove(d)
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
def before_save(self):
set_account_for_mode_of_payment(self)
@@ -614,6 +644,33 @@ class SalesInvoice(SellingController):
if not res:
throw(_("Customer {0} does not belong to project {1}").format(self.customer,self.project))
def set_inter_company_account(self):
"""
Set intercompany account for inter warehouse transactions
This account will be used in case billing company and internal customer's
representation company is same
"""
if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account')
if not unrealized_profit_loss_account:
msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format(
frappe.bold(self.company))
frappe.throw(msg)
self.unrealized_profit_loss_account = unrealized_profit_loss_account
def is_internal_transfer(self):
"""
It will an internal transfer if its an internal customer and representation
company is same as billing company
"""
if self.is_internal_customer and (self.represents_company == self.company):
return True
return False
def validate_pos(self):
if self.is_return:
invoice_total = self.rounded_total or self.grand_total

View File

@@ -1870,7 +1870,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
@@ -1881,7 +1891,8 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country
def test_einvoice_json(self):
@@ -2271,4 +2282,4 @@ def add_taxes(doc):
"cost_center": "Main - TCP1",
"description": "Excise Duty",
"rate": 12
})
})

View File

@@ -12,37 +12,62 @@ from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
pass
def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None):
def get_party_details(inv):
party_type, party = '', ''
if inv.doctype == 'Sales Invoice':
party_type = 'Customer'
party = inv.customer
else:
party_type = 'Supplier'
party = inv.supplier
return party_type, party
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = ''
suppliers = []
parties = []
party_type, party = get_party_details(inv)
if not tax_withholding_category:
tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan'])
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
if not tax_withholding_category:
return
# if tax_withholding_category passed as an argument but not pan_no
if not pan_no:
pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan')
pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No
if pan_no:
suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})]
parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name')
if not suppliers:
suppliers.append(ref_doc.supplier)
if not parties:
parties.append(party)
fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company)
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
.format(tax_withholding_category, ref_doc.company))
.format(tax_withholding_category, inv.company))
tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company,
tax_details, fy, ref_doc.posting_date, pan_no)
if party_type == 'Customer' and not tax_details.cumulative_threshold:
# TCS is only chargeable on sum of invoiced value
frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
.format(tax_withholding_category, inv.company, party))
tax_row = get_tax_row(tax_details, tds_amount)
tax_amount, tax_deducted = get_tax_amount(
party_type, parties,
inv, tax_details,
fiscal_year, pan_no
)
if party_type == 'Supplier':
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
return tax_row
@@ -69,147 +94,254 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year):
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
def get_tax_row(tax_details, tds_amount):
return {
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
"category": "Total",
"add_deduct_tax": "Deduct",
"charge_type": "Actual",
"account_head": tax_details.account_head,
"tax_amount": tax_amount,
"description": tax_details.description,
"tax_amount": tds_amount
"account_head": tax_details.account_head
}
def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None):
fiscal_year, year_start_date, year_end_date = fiscal_year_details
tds_amount = 0
tds_deducted = 0
if tax_deducted:
# TCS already deducted on previous invoices
# So, TCS will be calculated by 'Previous Row Total'
def _get_tds(amount, rate):
if amount <= 0:
return 0
return amount * rate / 100
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{
'pan_no': pan_no,
'fiscal_year': fiscal_year
}, 'name')
ldc = ''
if ldc_name:
ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name)
entries = frappe.db.sql("""
select voucher_no, credit
from `tabGL Entry`
where company = %s and
party in %s and fiscal_year=%s and credit > 0
and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries]
advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company)
tds_vouchers = vouchers + advance_vouchers
if tds_vouchers:
tds_deducted = frappe.db.sql("""
SELECT sum(credit) FROM `tabGL Entry`
WHERE
account=%s and fiscal_year=%s and credit > 0
and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))),
((tax_details.account_head, fiscal_year) + tuple(tds_vouchers)))
tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0
if tds_deducted:
if ldc:
limit_consumed = frappe.db.get_value('Purchase Invoice',
{
'supplier': ('in', suppliers),
'apply_tds': 1,
'docstatus': 1
}, 'sum(net_total)')
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total,
ldc.certificate_limit):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
if taxes_excluding_tcs:
# chargeable amount is the total amount after other charges are applied
row.update({
"charge_type": "On Previous Row Total",
"row_id": len(taxes_excluding_tcs),
"rate": tax_details.rate
})
else:
tds_amount = _get_tds(net_total, tax_details.rate)
else:
supplier_credit_amount = frappe.get_all('Purchase Invoice',
fields = ['sum(net_total)'],
filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
# if only TCS is to be charged, then net total is chargeable amount
row.update({
"charge_type": "On Net Total",
"rate": tax_details.rate
})
supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
return row
jv_supplier_credit_amt = frappe.get_all('Journal Entry Account',
fields = ['sum(credit_in_account_currency)'],
filters = {
'parent': ('in', vouchers), 'docstatus': 1,
'party': ('in', suppliers),
'reference_type': ('not in', ['Purchase Invoice'])
}, as_list=1)
def get_tax_row_for_tds(tax_details, tax_amount):
return {
"category": "Total",
"charge_type": "Actual",
"tax_amount": tax_amount,
"add_deduct_tax": "Deduct",
"description": tax_details.description,
"account_head": tax_details.account_head
}
supplier_credit_amount += (jv_supplier_credit_amt[0][0]
if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0)
def get_lower_deduction_certificate(fiscal_year, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
if ldc_name:
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
supplier_credit_amount += net_total
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
supplier_credit_amount -= debit_note_amount
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
tax_deducted = 0
if taxable_vouchers:
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
ldc.certificate_limit):
tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate,
tax_details)
tax_amount = 0
posting_date = inv.posting_date
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
if tax_deducted:
net_total = inv.net_total
if ldc:
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
else:
tds_amount = _get_tds(supplier_credit_amount, tax_details.rate)
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = get_tds_amount(
ldc, parties, inv, tax_details,
fiscal_year_details, tax_deducted, vouchers
)
elif party_type == 'Customer':
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(
parties, inv, tax_details,
fiscal_year_details, vouchers, advance_vouchers
)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
filters = {
dr_or_cr: ['>', 0],
'company': company,
'party_type': party_type,
'party': ['in', parties],
'fiscal_year': fiscal_year,
'is_opening': 'No',
'is_cancelled': 0
}
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
# for advance vouchers, debit and credit is reversed
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
filters = {
dr_or_cr: ['>', 0],
'is_opening': 'No',
'is_cancelled': 0,
'party_type': party_type,
'party': ['in', parties],
'against_voucher': ['is', 'not set']
}
if fiscal_year:
filters['fiscal_year'] = fiscal_year
if company:
filters['company'] = company
if from_date and to_date:
filters['posting_date'] = ['between', (from_date, to_date)]
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
'is_cancelled': 0,
'credit': ['>', 0],
'fiscal_year': fiscal_year,
'account': tax_details.account_head,
'voucher_no': ['in', taxable_vouchers],
}
field = "sum(credit)"
return frappe.db.get_value('GL Entry', filters, field) or 0.0
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1,
'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice')
}, 'sum(credit_in_account_currency)') or 0.0
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get('threshold', 0)
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
inv.posting_date, tax_deducted,
inv.net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
return tds_amount
def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None):
condition = "fiscal_year=%s" % fiscal_year
def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
tcs_amount = 0
fiscal_year, _, _ = fiscal_year_details
# sum of debit entries made from sales invoices
invoiced_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'party': ['in', parties],
'company': inv.company,
'voucher_no': ['in', vouchers],
}, 'sum(debit)') or 0.0
# sum of credit entries made from PE / JV with unset 'against voucher'
advance_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'party': ['in', parties],
'company': inv.company,
'voucher_no': ['in', adv_vouchers],
}, 'sum(credit)') or 0.0
# sum of credit entries made from sales invoice
credit_note_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'credit': ['>', 0],
'party': ['in', parties],
'fiscal_year': fiscal_year,
'company': inv.company,
'voucher_type': 'Sales Invoice',
}, 'sum(credit)') or 0.0
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)):
chargeable_amt = total_invoiced_amt - cumulative_threshold
tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
return tcs_amount
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
tds_amount = 0
limit_consumed = frappe.db.get_value('Purchase Invoice', {
'supplier': ('in', parties),
'apply_tds': 1,
'docstatus': 1
}, 'sum(net_total)')
if is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
posting_date, limit_consumed,
net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
_, year_start_date, year_end_date = fiscal_year_details
filters = {
'supplier': ['in', suppliers],
'is_return': 1,
'docstatus': 1,
'posting_date': ['between', (year_start_date, year_end_date)]
}
fields = ['abs(sum(net_total)) as net_total']
if company:
condition += "and company =%s" % (company)
if from_date and to_date:
condition += "and posting_date between %s and %s" % (from_date, to_date)
filters['company'] = company
## Appending the same supplier again if length of suppliers list is 1
## since tuple of single element list contains None, For example ('Test Supplier 1', )
## and the below query fails
if len(suppliers) == 1:
suppliers.append(suppliers[0])
return frappe.db.sql_list("""
select distinct voucher_no
from `tabGL Entry`
where party in %s and %s and debit > 0
and is_opening = 'No'
""", (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
condition = "and 1=1"
if company:
condition = " and company=%s " % company
if len(suppliers) == 1:
suppliers.append(suppliers[0])
return flt(frappe.db.sql("""
select abs(sum(net_total))
from `tabPurchase Invoice`
where supplier in %s and is_return=1 and docstatus=1
and posting_date between %s and %s %s
""", (tuple(suppliers), year_start_date, year_end_date, condition)))
return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
@@ -227,4 +359,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount,
certificate_limit > deducted_amount):
valid = True
return valid
return valid

View File

@@ -9,7 +9,7 @@ from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"]
test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod
@@ -18,6 +18,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
create_records()
create_tax_with_holding_category()
def tearDown(self):
cancel_invoices()
def test_cumulative_threshold_tds(self):
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = []
@@ -128,9 +131,59 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
si = create_sales_invoice(customer = "Test TCS Customer")
si.submit()
invoices.append(si)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
si = create_sales_invoice(customer = "Test TCS Customer", rate=12000)
si.submit()
# assert tax collection on total invoice amount created until now
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
self.assertEqual(tcs_charged, 200)
self.assertEqual(si.grand_total, 12200)
invoices.append(si)
# TCS is already collected once, so going forward system will collect TCS on every invoice
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
si.submit()
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
self.assertEqual(tcs_charged, 500)
invoices.append(si)
#delete invoices to avoid clashing
for d in invoices:
d.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
'docstatus': 1
}, pluck="name")
sales_invoices = frappe.get_all("Sales Invoice", {
'customer': 'Test TCS Customer',
'docstatus': 1
}, pluck="name")
for d in purchase_invoices:
frappe.get_doc('Purchase Invoice', d).cancel()
for d in sales_invoices:
frappe.get_doc('Sales Invoice', d).cancel()
def create_purchase_invoice(**args):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
args = frappe._dict(args)
pi = frappe.get_doc({
@@ -145,7 +198,7 @@ def create_purchase_invoice(**args):
"taxes": [],
"items": [{
'doctype': 'Purchase Invoice Item',
'item_code': item.name,
'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
@@ -156,6 +209,33 @@ def create_purchase_invoice(**args):
pi.save()
return pi
def create_sales_invoice(**args):
# return sales invoice doc object
item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name")
args = frappe._dict(args)
si = frappe.get_doc({
"doctype": "Sales Invoice",
"posting_date": today(),
"customer": args.customer,
"company": '_Test Company',
"taxes_and_charges": "",
"currency": "INR",
"debit_to": "Debtors - _TC",
"taxes": [],
"items": [{
'doctype': 'Sales Invoice Item',
'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
'expense_account': 'Cost of Goods Sold - _TC'
}]
})
si.save()
return si
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
@@ -168,7 +248,17 @@ def create_records():
"doctype": "Supplier",
}).insert()
# create an item
for name in ['Test TCS Customer']:
if frappe.db.exists('Customer', name):
continue
frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": name,
"doctype": "Customer"
}).insert()
# create item
if not frappe.db.exists('Item', "TDS Item"):
frappe.get_doc({
"doctype": "Item",
@@ -178,7 +268,16 @@ def create_records():
"is_stock_item": 0,
}).insert()
# create an account
if not frappe.db.exists('Item', "TCS Item"):
frappe.get_doc({
"doctype": "Item",
"item_code": "TCS Item",
"item_name": "TCS Item",
"item_group": "All Item Groups",
"is_stock_item": 1
}).insert()
# create tds account
if not frappe.db.exists("Account", "TDS - _TC"):
frappe.get_doc({
'doctype': 'Account',
@@ -189,6 +288,17 @@ def create_records():
'root_type': 'Asset'
}).insert()
# create tcs account
if not frappe.db.exists("Account", "TCS - _TC"):
frappe.get_doc({
'doctype': 'Account',
'company': '_Test Company',
'account_name': 'TCS',
'parent_account': 'Duties and Taxes - _TC',
'report_type': 'Balance Sheet',
'root_type': 'Liability'
}).insert()
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
@@ -210,6 +320,23 @@ def create_tax_with_holding_category():
}]
}).insert()
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Cumulative Threshold TCS",
"category_name": "10% TCS",
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
}],
"accounts": [{
'company': '_Test Company',
'account': 'TCS - _TC'
}]
}).insert()
# Single thresold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
frappe.get_doc({

View File

@@ -364,7 +364,7 @@ class ReceivablePayableReport(object):
payment_terms_details = frappe.db.sql("""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
ps.due_date, ps.payment_amount, ps.description, ps.paid_amount
ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -395,13 +395,13 @@ class ReceivablePayableReport(object):
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description,
"paid": d.paid_amount,
"paid": d.paid_amount + d.discounted_amount,
"credit_note": 0.0,
"outstanding": invoiced - d.paid_amount
"outstanding": invoiced - d.paid_amount - d.discounted_amount
}))
if d.paid_amount:
row['paid'] -= d.paid_amount
row['paid'] -= d.paid_amount + d.discounted_amount
def allocate_closing_to_term(self, row, term, key):
if row[key]:

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports['Billed Items To Be Received'] = {
'filters': [
{
'label': __('Company'),
'fieldname': 'company',
'fieldtype': 'Link',
'options': 'Company',
'reqd': 1,
'default': frappe.defaults.get_default('Company')
},
{
'label': __('As on Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'reqd': 1,
'default': get_today()
},
{
'label': __('Purchase Invoice'),
'fieldname': 'purchase_invoice',
'fieldtype': 'Link',
'options': 'Purchase Invoice'
}
]
};

View File

@@ -0,0 +1,39 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-03-30 09:35:38.683028",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-03-31 08:48:30.944429",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Billed Items To Be Received",
"owner": "Administrator",
"prepared_report": 0,
"query": "",
"ref_doctype": "Purchase Invoice",
"report_name": "Billed Items To Be Received",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Purchase User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
},
{
"role": "Stock User"
}
]
}

View File

@@ -0,0 +1,107 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
data = get_data(filters) or []
columns = get_columns()
return columns, data
def get_data(report_filters):
filters = get_report_filters(report_filters)
fields = get_report_fields()
return frappe.get_all('Purchase Invoice',
fields= fields, filters=filters)
def get_report_filters(report_filters):
filters = [['Purchase Invoice','company','=',report_filters.get('company')],
['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1],
['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]]
if report_filters.get('purchase_invoice'):
filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]])
return filters
def get_report_fields():
fields = []
for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']:
fields.append('`tabPurchase Invoice`.`{}`'.format(p_field))
for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']:
fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field))
return fields
def get_columns():
return [
{
'label': _('Purchase Invoice'),
'fieldname': 'name',
'fieldtype': 'Link',
'options': 'Purchase Invoice',
'width': 170
},
{
'label': _('Supplier'),
'fieldname': 'supplier',
'fieldtype': 'Link',
'options': 'Supplier',
'width': 120
},
{
'label': _('Posting Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'width': 100
},
{
'label': _('Item Code'),
'fieldname': 'item_code',
'fieldtype': 'Link',
'options': 'Item',
'width': 100
},
{
'label': _('Item Name'),
'fieldname': 'item_name',
'fieldtype': 'Data',
'width': 100
},
{
'label': _('UOM'),
'fieldname': 'uom',
'fieldtype': 'Link',
'options': 'UOM',
'width': 100
},
{
'label': _('Invoiced Qty'),
'fieldname': 'qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Received Qty'),
'fieldname': 'received_qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Rate'),
'fieldname': 'rate',
'fieldtype': 'Currency',
'width': 100
},
{
'label': _('Amount'),
'fieldname': 'amount',
'fieldtype': 'Currency',
'width': 100
}
]

View File

@@ -0,0 +1,81 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname": "fiscal_year",
"label": __("Fiscal Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1,
"on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date"),
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date"),
},
{
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book",
},
{
"fieldname": "dimension",
"label": __("Select Dimension"),
"fieldtype": "Select",
"options": get_accounting_dimension_options(),
"reqd": 1,
},
],
"formatter": erpnext.financial_statements.formatter,
"tree": true,
"name_field": "account",
"parent_field": "parent_account",
"initial_depth": 3
}
});
function get_accounting_dimension_options() {
let options =["", "Cost Center", "Project"];
frappe.db.get_list('Accounting Dimension',
{fields:['document_type']}).then((res) => {
res.forEach((dimension) => {
options.push(dimension.document_type);
});
});
return options
}

View File

@@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-09 16:48:59.548018",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-09 16:48:59.548018",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dimension-wise Accounts Balance Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Dimension-wise Accounts Balance Report",
"report_type": "Script Report",
"roles": []
}

View File

@@ -0,0 +1,203 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import (flt, cstr)
from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from six import itervalues
def execute(filters=None):
validate_filters(filters)
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
if not dimension_items_list:
return [], []
dimension_items_list = [''.join(d) for d in dimension_items_list]
columns = get_columns(dimension_items_list)
data = get_data(filters, dimension_items_list)
return columns, data
def get_data(filters, dimension_items_list):
company_currency = erpnext.get_company_currency(filters.company)
acc = frappe.db.sql("""
select
name, account_number, parent_account, lft, rgt, root_type,
report_type, account_name, include_in_gross, account_type, is_group
from
`tabAccount`
where
company=%s
order by lft""", (filters.company), as_dict=True)
if not acc:
return None
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount`
where company=%s""", (filters.company))[0]
account = frappe.db.sql_list("""select name from `tabAccount`
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
gl_entries_by_account = {}
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
out = filter_out_zero_value_rows(out, parent_children_map)
return out
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
for item in dimension_items_list:
condition = get_condition(filters.from_date, item, filters.dimension)
if account:
condition += " and account in ({})"\
.format(", ".join([frappe.db.escape(d) for d in account]))
gl_filters = {
"company": filters.get("company"),
"from_date": filters.get("from_date"),
"to_date": filters.get("to_date"),
"finance_book": cstr(filters.get("finance_book"))
}
gl_filters['item'] = ''.join(item)
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.db.get_value("Company",
filters.company, 'default_finance_book')
for key, value in filters.items():
if value:
gl_filters.update({
key: value
})
gl_entries = frappe.db.sql("""
select
posting_date, account, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency
from
`tabGL Entry`
where
company=%(company)s
{condition}
and posting_date <= %(to_date)s
and is_cancelled = 0
order by account, posting_date""".format(
condition=condition),
gl_filters, as_dict=True) #nosec
for entry in gl_entries:
entry['dimension_item'] = ''.join(item)
gl_entries_by_account.setdefault(entry.account, []).append(entry)
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
for entries in itervalues(gl_entries_by_account):
for entry in entries:
d = accounts_by_name.get(entry.account)
if not d:
frappe.msgprint(
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
raise_exception=1
)
for item in dimension_items_list:
if item == entry.dimension_item:
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
data = []
for d in accounts:
has_value = False
row = {
"account": d.name,
"parent_account": d.parent_account,
"indent": d.indent,
"from_date": filters.from_date,
"to_date": filters.to_date,
"currency": company_currency,
"account_name": ('{} - {}'.format(d.account_number, d.account_name)
if d.account_number else d.account_name)
}
for item in dimension_items_list:
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
if abs(row[frappe.scrub(item)]) >= 0.005:
# ignore zero values
has_value = True
row["has_value"] = has_value
data.append(row)
return data
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
for item in dimension_items_list:
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
def get_condition(from_date, item, dimension):
conditions = []
if from_date:
conditions.append("posting_date >= %(from_date)s")
if dimension:
if dimension not in ['Cost Center', 'Project']:
if dimension in ['Customer', 'Supplier']:
dimension = 'Party'
else:
dimension = 'Voucher No'
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
conditions.append(txt)
return " and {}".format(" and ".join(conditions)) if conditions else ""
def get_dimension_items_list(dimension, company):
meta = frappe.get_meta(dimension, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
filters = {}
if 'company' in fieldnames:
filters['company'] = company
return frappe.get_all(dimension, filters, as_list=True)
def get_columns(dimension_items_list, accumulated_values=1, company=None):
columns = [{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
}]
if company:
columns.append({
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
})
for item in dimension_items_list:
columns.append({
"fieldname": frappe.scrub(item),
"label": item,
"fieldtype": "Currency",
"options": "currency",
"width": 150
})
return columns

View File

@@ -318,6 +318,7 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,

View File

@@ -13,6 +13,8 @@
"po_required",
"pr_required",
"maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
@@ -89,6 +91,23 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"mandatory_depends_on": "maintain_same_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
}
],
"icon": "fa fa-cog",
@@ -96,7 +115,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:00:23.276329",
"modified": "2021-04-04 20:01:44.087066",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -697,7 +697,9 @@ class AccountsController(TransactionBase):
total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt)
if total_billed_amt - max_allowed_amt > 0.01:
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
@@ -882,29 +884,35 @@ class AccountsController(TransactionBase):
date = self.get("due_date")
due_date = date or posting_date
if party_account_currency == self.company_currency:
grand_total = self.get("base_rounded_total") or self.base_grand_total
else:
grand_total = self.get("rounded_total") or self.grand_total
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"):
grand_total -= self.get("total_advance")
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if not self.get("payment_schedule"):
if self.get("payment_terms_template"):
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total)
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total)
for item in data:
self.append("payment_schedule", item)
else:
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total)
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total)
self.append("payment_schedule", data)
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount'))
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.outstanding = d.payment_amount
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -940,22 +948,28 @@ class AccountsController(TransactionBase):
if self.get("payment_schedule"):
total = 0
base_total = 0
for d in self.get("payment_schedule"):
total += flt(d.payment_amount)
base_total += flt(d.base_payment_amount)
if party_account_currency == self.company_currency:
total = flt(total, self.precision("base_grand_total"))
grand_total = flt(self.get("base_rounded_total") or self.base_grand_total, self.precision('base_grand_total'))
else:
total = flt(total, self.precision("grand_total"))
grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total'))
if self.get("total_advance"):
grand_total -= self.get("total_advance")
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if total != flt(grand_total, self.precision("grand_total")):
if self.get("total_advance"):
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
print(grand_total, base_grand_total)
if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self):
@@ -1195,7 +1209,7 @@ def update_invoice_status():
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_date=None):
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
return
@@ -1203,14 +1217,14 @@ def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_
schedule = []
for d in terms_doc.get("terms"):
term_details = get_payment_term_details(d, posting_date, grand_total, bill_date)
term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
schedule.append(term_details)
return schedule
@frappe.whitelist()
def get_payment_term_details(term, posting_date=None, grand_total=None, bill_date=None):
def get_payment_term_details(term, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
term_details = frappe._dict()
if isinstance(term, text_type):
term = frappe.get_doc("Payment Term", term)
@@ -1219,18 +1233,24 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
term_details.discount_type = term.discount_type
term_details.discount = term.discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
term_details.discount_date = get_discount_date(term, bill_date)
elif posting_date:
term_details.due_date = get_due_date(term, posting_date)
term_details.discount_date = get_discount_date(term, posting_date)
if getdate(term_details.due_date) < getdate(posting_date):
term_details.due_date = posting_date
term_details.mode_of_payment = term.mode_of_payment
return term_details
def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
date = bill_date or posting_date
@@ -1242,6 +1262,16 @@ def get_due_date(term, posting_date=None, bill_date=None):
due_date = add_months(get_last_day(date), term.credit_months)
return due_date
def get_discount_date(term, posting_date=None, bill_date=None):
discount_validity = None
date = bill_date or posting_date
if term.discount_validity_based_on == "Day(s) after invoice date":
discount_validity = add_days(date, term.discount_validity)
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
discount_validity = add_days(get_last_day(date), term.discount_validity)
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
discount_validity = add_months(get_last_day(date), term.discount_validity)
return discount_validity
def get_supplier_block_status(party_name):
"""
@@ -1395,8 +1425,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
def get_new_child_item(item_row):
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):

View File

@@ -201,10 +201,14 @@ class StatusUpdater(Document):
get_allowance_for(item['item_code'], self.item_allowance,
self.global_qty_allowance, self.global_amount_allowance, qty_or_amount)
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
role_allowed_to_over_deliver_receive = frappe.db.get_single_value('Stock Settings', 'role_allowed_to_over_deliver_receive')
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
role = role_allowed_to_over_deliver_receive if qty_or_amount == 'qty' else role_allowed_to_over_bill
if overflow_percent - allowance > 0.01:
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed']

View File

@@ -15,6 +15,8 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_ra
class calculate_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def calculate(self):
@@ -340,10 +342,18 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
def get_final_current_tax_amount(self, tax, current_tax_amount):
# Some countries need individual tax components to be rounded
# Handeled via regional doctypess
if tax.account_head in frappe.flags.round_off_applicable_accounts:
current_tax_amount = round(current_tax_amount, 0)
return current_tax_amount
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -701,6 +711,15 @@ def get_itemised_tax_breakup_html(doc):
)
)
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
account_list = get_regional_round_off_accounts(company, account_list)
return account_list
@erpnext.allow_regional
def get_regional_round_off_accounts(company, account_list):
pass
@erpnext.allow_regional
def update_itemised_tax_data(doc):

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Integration Item', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,119 @@
{
"actions": [],
"creation": "2021-03-13 12:24:28.511716",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"integration_item_name",
"erpnext_item_code"
],
"fields": [
{
"fieldname": "integration_item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Integration Item Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "erpnext_item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "ERPNext Item Code ",
"options": "Item",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-13 18:11:18.798940",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Integration Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Item Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"select": 1,
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"select": 1,
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"select": 1,
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Maintenance User",
"select": 1,
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"select": 1,
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "integration_item_name",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class IntegrationItem(Document):
pass

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestIntegrationItem(unittest.TestCase):
pass

View File

@@ -116,7 +116,7 @@ def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_
]
}
if not is_item_exists(item_dict, attributes, variant_of=variant_of):
if not item_exists(item_dict, attributes, variant_of=variant_of):
item_details = get_item_details(shopify_item)
name = ''
@@ -255,11 +255,16 @@ def get_item_details(shopify_item):
["name", "stock_uom", "item_name"], as_dict=1)
return item_details
def is_item_exists(shopify_item, attributes=None, variant_of=None):
if variant_of:
name = variant_of
else:
name = frappe.db.get_value("Item", {"item_name": shopify_item.get("item_name")})
def item_exists(shopify_item, attributes=None, variant_of=None):
name = ''
name = frappe.db.get_value('Integration Item', {'integration_item_name': shopify_item.get("item_name")},
'erpnext_item_code')
if not name:
if variant_of:
name = variant_of
else:
name = frappe.db.get_value("Item", {"item_name": shopify_item.get("item_name")})
if name:
item = frappe.get_doc("Item", name)

View File

@@ -6,10 +6,12 @@ import frappe
import unittest, os, json
from frappe.utils import cstr
from erpnext.erpnext_integrations.connectors.shopify_connection import create_order
from erpnext.erpnext_integrations.connectors.shopify_connection import create_order, validate_customer, validate_item
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
from frappe.core.doctype.data_import.data_import import import_doc
from erpnext.stock.doctype.item.test_item import make_item as make_erpnext_item
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class ShopifySettings(unittest.TestCase):
@@ -96,3 +98,239 @@ class ShopifySettings(unittest.TestCase):
where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0]
self.assertEqual(delivery_note_count, len(shopify_order.get("order").get("fulfillments")))
def test_product_sync_using_integration_item(self):
# Create erpnext item
item = make_erpnext_item("_Test Shopify Item")
if not frappe.db.get_value("Customer", "_Test Shopify Customer"):
customer = get_customer_dict("_Test Shopify Customer")
customer['shopify_customer_id'] = "5049858588862"
frappe.get_doc(customer).insert(ignore_permissions=True)
# Add Integration Item
if not frappe.db.get_value("Integration Item", {"integration_item_name": "Protein Bar"}):
frappe.get_doc({
"doctype":"Integration Item",
"integration_item_name": "Protein Bar",
"erpnext_item_code": "_Test Shopify Item"
}).insert()
item = {
"id": 6565573427390,
"title": "Protein Bar",
"body_html": "",
"vendor": "Maple Stores Mumbai",
"product_type": "",
"created_at": "2021-03-12T11:42:04+05:30",
"handle": "macbook-air-copper",
"updated_at": "2021-03-12T13:40:18+05:30",
"published_at": "2021-03-12T11:42:05+05:30",
"template_suffix": "",
"status": "active",
"published_scope": "web",
"tags": "",
"variants": [
{
"id": 39351752425662,
"product_id": 6565573427390,
"title": "Default Title",
"price": "140000.00",
"sku": "",
"position": 1,
"inventory_policy": "deny",
"fulfillment_service": "manual",
"inventory_management": "shopify",
"option1": "Default Title",
"created_at": "2021-03-12T11:42:04+05:30",
"updated_at": "2021-03-12T13:38:35+05:30",
"barcode": "",
"grams": 0,
"weight": 0.0,
"weight_unit": "kg",
"inventory_item_id": 41445821972670,
"inventory_quantity": 14,
"old_inventory_quantity": 14,
}
],
"options": [
{
"id": 8439626989758,
"product_id": 6565573427390,
"name": "Title",
"position": 1,
"values": [
"Default Title"
]
}
],
"images": [],
}
shopify_order_json = {
"id": 3669049704638,
"created_at": "2021-03-12T13:38:34+05:30",
"updated_at": "2021-03-12T13:38:35+05:30",
"number": 8,
"note": "",
"token": "88d2c4051b4b13e268ebb6ed409db82a",
"gateway": "manual",
"total_price": "165200.00",
"subtotal_price": "140000.00",
"total_weight": 0,
"total_tax": "25200.00",
"currency": "INR",
"financial_status": "pending",
"total_discounts": "0.00",
"total_line_items_price": "140000.00",
"name": "#1008",
"total_price_usd": "2272.11",
"user_id": 71496466622,
"location_id": 61178446014,
"processed_at": "2021-03-12T13:38:34+05:30",
"customer_locale": "en",
"app_id": 1354745,
"order_number": 1008,
"discount_applications": [],
"discount_codes": [],
"note_attributes": [],
"payment_gateway_names": [
"manual"
],
"processing_method": "manual",
"source_name": "shopify_draft_order",
"tax_lines": [],
"tags": "",
"contact_email": "frappe@maplestores.com",
"presentment_currency": "INR",
"total_line_items_price_set": {
"shop_money": {
"amount": "140000.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "140000.00",
"currency_code": "INR"
}
},
"total_discounts_set": {
"shop_money": {
"amount": "0.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "0.00",
"currency_code": "INR"
}
},
"total_shipping_price_set": {
"shop_money": {
"amount": "0.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "0.00",
"currency_code": "INR"
}
},
"subtotal_price_set": {
"shop_money": {
"amount": "140000.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "140000.00",
"currency_code": "INR"
}
},
"total_price_set": {
"shop_money": {
"amount": "165200.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "165200.00",
"currency_code": "INR"
}
},
"total_tax_set": {
"shop_money": {
"amount": "25200.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "25200.00",
"currency_code": "INR"
}
},
"line_items": [
{
"id": 9645225836734,
"variant_id": 39351752425662,
"title": "Protein Bar",
"quantity": 1,
"sku": "",
"vendor": "Maple Stores Mumbai",
"fulfillment_service": "manual",
"product_id": 6565573427390,
"name": "Protein Bar",
"variant_inventory_management": "shopify",
"properties": [],
"product_exists": 1,
"fulfillable_quantity": 1,
"grams": 0,
"price": "140000.00",
"total_discount": "0.00",
"price_set": {
"shop_money": {
"amount": "140000.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "140000.00",
"currency_code": "INR"
}
},
"total_discount_set": {
"shop_money": {
"amount": "0.00",
"currency_code": "INR"
},
"presentment_money": {
"amount": "0.00",
"currency_code": "INR"
}
},
"discount_allocations": [],
"duties": [],
"tax_lines": []
}
],
"fulfillments": [],
"refunds": [],
"total_tip_received": "0.0",
"shipping_lines": [],
"customer": {
"id": 5049858588862,
"email": "frappe@maplestores.com",
"created_at": "2021-03-10T19:57:40+05:30",
"updated_at": "2021-03-12T13:38:35+05:30",
"first_name": "_Test Shopify Customer",
"last_name": "",
"orders_count": 5,
"state": "disabled",
"total_spent": "731600.00",
"last_order_id": 3669049704638,
"note": "",
"tags": "",
"last_order_name": "#1008",
"currency": "INR",
"accepts_marketing_updated_at": "2021-03-10T19:57:40+05:30",
}
}
# Create Order
make_item("_Test Warehouse - _TC", item)
create_order(shopify_order_json, self.shopify_settings, False, company="_Test Company")
sales_order_doc = frappe.get_doc("Sales Order", {"shopify_order_id": "3669049704638"})
self.assertEqual(sales_order_doc.items[0].item_code, "_Test Shopify Item")

View File

@@ -37,9 +37,9 @@ def get_webhook_address(connector_name, method, exclude_uri=False):
try:
url = frappe.request.url
except RuntimeError:
url = "http://localhost:8000"
url = "https://localhost:8000"
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
server_url = 'https://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
return server_url

View File

@@ -255,14 +255,17 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
"on_trash": "erpnext.regional.check_deletion_permission"
"on_trash": "erpnext.regional.check_deletion_permission",
"validate": ["erpnext.regional.india.utils.update_taxable_values"]
},
"Purchase Invoice": {
"validate": [
"erpnext.regional.india.utils.update_grand_total_for_rcm",
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
"erpnext.regional.india.utils.update_itc_availed_fields",
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
"erpnext.regional.united_arab_emirates.utils.validate_returns"
]
"erpnext.regional.united_arab_emirates.utils.validate_returns",
"erpnext.regional.india.utils.update_taxable_values"
]
},
"Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
@@ -402,9 +405,9 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields'
},
'United Arab Emirates': {

View File

@@ -753,5 +753,9 @@ erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v12_0.create_itc_reversal_custom_fields #1
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v12_0.create_taxable_value_field
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v13_0.update_payment_terms_outstanding

View File

@@ -0,0 +1,16 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company or not frappe.db.count('E Invoice User'):
return
frappe.reload_doc("regional", "doctype", "e_invoice_user")
for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
company_name = frappe.db.sql("""
select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
""", (creds.get('gstin')))
if company_name and len(company_name) > 0:
frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])

View File

@@ -0,0 +1,115 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.regional.india.utils import get_gst_accounts
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
if not company:
return
frappe.reload_doc("regional", "doctype", "gst_settings")
frappe.reload_doc("accounts", "doctype", "gst_account")
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
custom_fields = {
'Journal Entry': [
dict(fieldname='reversal_type', label='Reversal Type',
fieldtype='Select', insert_after='voucher_type', print_hide=1,
options="As per rules 42 & 43 of CGST Rules\nOthers",
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_address', label='Company Address',
fieldtype='Link', options='Address', insert_after='reversal_type',
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
fetch_from='company_address.gstin',
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
],
'Purchase Invoice': [
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
default="All Other ITC")
],
'Purchase Invoice Item': [
dict(fieldname='taxable_value', label='Taxable Value',
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
print_hide=1)
]
}
create_custom_fields(custom_fields, update=True)
# Patch ITC Availed fields from Data to Currency
# Patch Availed ITC for current fiscal_year
gst_accounts = get_gst_accounts(only_non_reverse_charge=1)
frappe.db.sql("""
UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency'
WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax',
'itc_cess_amount')
""")
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0'
WHERE trim(coalesce(itc_integrated_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0'
WHERE trim(coalesce(itc_state_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0'
WHERE trim(coalesce(itc_central_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0'
WHERE trim(coalesce(itc_cess_amount, '')) = '' """)
# Get purchase invoices
invoices = frappe.get_all('Purchase Invoice',
{'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')},
['name'])
amount_map = {}
if invoices:
invoice_list = set([d.name for d in invoices])
# Get GST applied
amounts = frappe.db.sql("""
SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount
FROM `tabPurchase Taxes and Charges`
where parent in %s
GROUP BY parent, account_head
""", (invoice_list), as_dict=1)
for d in amounts:
amount_map.setdefault(d.parent,
{
'itc_integrated_tax': 0,
'itc_state_tax': 0,
'itc_central_tax': 0,
'itc_cess_amount': 0
})
if d.account_head in gst_accounts.get('igst_account'):
amount_map[d.parent]['itc_integrated_tax'] += d.amount
if d.account_head in gst_accounts.get('cgst_account'):
amount_map[d.parent]['itc_central_tax'] += d.amount
if d.account_head in gst_accounts.get('sgst_account'):
amount_map[d.parent]['itc_state_tax'] += d.amount
if d.account_head in gst_accounts.get('cess_account'):
amount_map[d.parent]['itc_cess_amount'] += d.amount
for invoice, values in amount_map.items():
frappe.db.set_value('Purchase Invoice', invoice, {
'itc_integrated_tax': values.get('itc_integrated_tax'),
'itc_central_tax': values.get('itc_central_tax'),
'itc_state_tax': values['itc_state_tax'],
'itc_cess_amount': values['itc_cess_amount'],
})

View File

@@ -0,0 +1,18 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_fields = {
'Sales Invoice Item': [
dict(fieldname='taxable_value', label='Taxable Value',
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
print_hide=1)
]
}
create_custom_fields(custom_fields, update=True)

View File

@@ -0,0 +1,15 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("accounts", "doctype", "Payment Schedule")
if frappe.db.count('Payment Schedule'):
frappe.db.sql('''
UPDATE
`tabPayment Schedule` ps
SET
ps.outstanding = (ps.payment_amount - ps.paid_amount)
''')

View File

@@ -78,7 +78,7 @@ class Task(NestedSet):
def validate_dependencies_for_template_task(self):
if self.is_template:
self.validate_depends_on_tasks()
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
@@ -209,18 +209,25 @@ def check_if_child_exists(name):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
return frappe.db.sql(""" select name from `tabProject`
where %(key)s like %(txt)s
%(mcond)s
order by name
limit %(start)s, %(page_len)s""" % {
'key': searchfield,
'txt': frappe.db.escape('%' + txt + '%'),
'mcond':get_match_cond(doctype),
'start': start,
'page_len': page_len
})
from erpnext.controllers.queries import get_match_cond
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_columns = ", " + ", ".join(searchfields) if searchfields else ''
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
return frappe.db.sql(""" select name {search_columns} from `tabProject`
where %(key)s like %(txt)s
%(mcond)s
{search_condition}
order by name
limit %(start)s, %(page_len)s""".format(search_columns = search_columns,
search_condition=search_cond), {
'key': searchfield,
'txt': '%' + txt + '%',
'mcond':get_match_cond(doctype),
'start': start,
'page_len': page_len
})
@frappe.whitelist()

View File

@@ -204,14 +204,16 @@ class Timesheet(Document):
ts_detail.billing_rate = 0.0
@frappe.whitelist()
def get_projectwise_timesheet_data(project, parent=None):
cond = ''
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
condition = ''
if parent:
cond = "and parent = %(parent)s"
condition = "AND parent = %(parent)s"
if from_time and to_time:
condition += "AND from_time BETWEEN %(from_time)s AND %(to_time)s"
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1)
and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -198,6 +198,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this._super(doc, cdt, cdn);
},
batch_no: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn);
},
received_qty: function(doc, cdt, cdn) {
this.calculate_accepted_qty(doc, cdt, cdn)
},

View File

@@ -2,7 +2,9 @@
// License: GNU General Public License v3. See license.txt
erpnext.taxes_and_totals = erpnext.payments.extend({
setup: function() {},
setup: function() {
this.fetch_round_off_accounts();
},
apply_pricing_rule_on_item: function(item){
let effective_item_rate = item.price_list_rate;
@@ -151,6 +153,22 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
});
},
fetch_round_off_accounts: function() {
let me = this;
frappe.flags.round_off_applicable_accounts = [];
return frappe.call({
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
"args": {
"company": me.frm.doc.company,
"account_list": frappe.flags.round_off_applicable_accounts
},
callback: function(r) {
frappe.flags.round_off_applicable_accounts.push(...r.message);
}
});
},
determine_exclusive_rate: function() {
var me = this;
@@ -371,11 +389,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
} else if (tax.charge_type == "On Item Quantity") {
current_tax_amount = tax_rate * item.qty;
}
current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
get_final_tax_amount: function(tax, current_tax_amount) {
if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
current_tax_amount = Math.round(current_tax_amount);
}
return current_tax_amount;
},
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;

View File

@@ -596,13 +596,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
() => me.toggle_conversion_factor(item),
() => {
if (show_batch_dialog)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
(r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false;
}
});
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
(r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false;
}
});
},
() => {
// check if batch serial selector is disabled or not
if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
.then((value) => {
if(value) {
frappe.flags.hide_serial_batch_dialog = true;
}
});
},
() => {
// check if batch serial selector is disabled or not
@@ -639,6 +649,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let key = item.name;
me.apply_rule_on_other_items({key: item});
}
},
() => {
var company_currency = me.get_company_currency();
me.update_item_grid_labels(company_currency);
}
]);
}
@@ -1136,7 +1150,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(frappe.flags.dont_fetch_price_list_rate) {
return
}
if (!dont_fetch_price_list_rate &&
frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true);
@@ -1278,11 +1292,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
change_grid_labels: function(company_currency) {
var me = this;
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
company_currency, "items");
this.update_item_grid_labels(company_currency);
this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
this.frm.doc.currency, "items");
this.toggle_item_grid_columns(company_currency);
if(this.frm.fields_dict["operations"]) {
this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations");
@@ -1317,6 +1329,39 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.doc.party_account_currency, "advances");
}
this.update_payment_schedule_grid_labels(company_currency);
},
update_item_grid_labels: function(company_currency) {
this.frm.set_currency_labels([
"base_rate", "base_net_rate", "base_price_list_rate",
"base_amount", "base_net_amount", "base_rate_with_margin"
], company_currency, "items");
this.frm.set_currency_labels([
"rate", "net_rate", "price_list_rate", "amount",
"net_amount", "stock_uom_rate", "rate_with_margin"
], this.frm.doc.currency, "items");
},
update_payment_schedule_grid_labels: function(company_currency) {
const me = this;
if (this.frm.fields_dict["payment_schedule"]) {
this.frm.set_currency_labels(["base_payment_amount", "base_outstanding", "base_paid_amount"],
company_currency, "payment_schedule");
this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"],
this.frm.doc.currency, "payment_schedule");
var schedule_grid = this.frm.fields_dict["payment_schedule"].grid;
$.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function(i, fname) {
if (frappe.meta.get_docfield(schedule_grid.doctype, fname))
schedule_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
}
},
toggle_item_grid_columns: function(company_currency) {
const me = this;
// toggle columns
var item_grid = this.frm.fields_dict["items"].grid;
$.each(["base_rate", "base_price_list_rate", "base_amount"], function(i, fname) {
@@ -1336,9 +1381,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, (show && (me.frm.doc.currency != company_currency)));
});
// set labels
var $wrapper = $(this.frm.wrapper);
},
recalculate: function() {
@@ -1952,11 +1994,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
terms_template: doc.payment_terms_template,
posting_date: posting_date,
grand_total: doc.rounded_total || doc.grand_total,
base_grand_total: doc.base_rounded_total || doc.base_grand_total,
bill_date: doc.bill_date
},
callback: function(r) {
if(r.message && !r.exc) {
me.frm.set_value("payment_schedule", r.message);
const company_currency = me.get_company_currency();
this.update_payment_schedule_grid_labels(company_currency);
}
}
})
@@ -1964,6 +2009,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
payment_term: function(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn];
if(row.payment_term) {
frappe.call({
@@ -1972,12 +2018,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
term: row.payment_term,
bill_date: this.frm.doc.bill_date,
posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date,
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total,
base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total
},
callback: function(r) {
if(r.message && !r.exc) {
for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]);
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
}
}
}

View File

@@ -100,4 +100,4 @@ erpnext.accounts.dimensions = {
});
}
}
};
};

View File

@@ -0,0 +1,26 @@
{{
"SlNo": "{item.sr_no}",
"PrdDesc": "{item.description}",
"IsServc": "{item.is_service_item}",
"HsnCd": "{item.gst_hsn_code}",
"Barcde": "{item.barcode}",
"Unit": "{item.uom}",
"Qty": "{item.qty}",
"FreeQty": "{item.free_qty}",
"UnitPrice": "{item.base_rate}",
"TotAmt": "{item.base_amount}",
"Discount": "{item.discount_amount}",
"AssAmt": "{item.base_amount}",
"PrdSlNo": "{item.serial_no}",
"GstRt": "{item.tax_rate}",
"IgstAmt": "{item.igst_amount}",
"CgstAmt": "{item.cgst_amount}",
"SgstAmt": "{item.sgst_amount}",
"CesRt": "{item.cess_rate}",
"CesAmt": "{item.cess_amount}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
}}

View File

@@ -0,0 +1,109 @@
{{
"Version": "1.01",
"TranDtls": {{
"TaxSch": "{trans_details.tax_scheme}",
"SupTyp": "{trans_details.supply_type}",
"RegRev": "{trans_details.reverse_charge}",
"EcmGstin": "{trans_details.ecom_gstin}",
"IgstOnIntra": "{trans_details.igst_on_intra}"
}},
"DocDtls": {{
"Typ": "{doc_details.invoice_type}",
"No": "{doc_details.invoice_name}",
"Dt": "{doc_details.invoice_date}"
}},
"SellerDtls": {{
"Gstin": "{seller_details.gstin}",
"LglNm": "{seller_details.legal_name}",
"TrdNm": "{seller_details.trade_name}",
"Loc": "{seller_details.location}",
"Pin": "{seller_details.pincode}",
"Stcd": "{seller_details.state_code}",
"Addr1": "{seller_details.address_line1}",
"Addr2": "{seller_details.address_line2}",
"Ph": "{seller_details.phone}",
"Em": "{seller_details.email}"
}},
"BuyerDtls": {{
"Gstin": "{buyer_details.gstin}",
"LglNm": "{buyer_details.legal_name}",
"TrdNm": "{buyer_details.trade_name}",
"Addr1": "{buyer_details.address_line1}",
"Addr2": "{buyer_details.address_line2}",
"Loc": "{buyer_details.location}",
"Pin": "{buyer_details.pincode}",
"Stcd": "{buyer_details.state_code}",
"Ph": "{buyer_details.phone}",
"Em": "{buyer_details.email}",
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
"Nm": "{dispatch_details.company_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
"Pin": "{dispatch_details.pincode}",
"Stcd": "{dispatch_details.state_code}"
}},
"ShipDtls": {{
"Gstin": "{shipping_details.gstin}",
"LglNm": "{shipping_details.legal_name}",
"TrdNm": "{shipping_details.trader_name}",
"Addr1": "{shipping_details.address_line1}",
"Addr2": "{shipping_details.address_line2}",
"Loc": "{shipping_details.location}",
"Pin": "{shipping_details.pincode}",
"Stcd": "{shipping_details.state_code}"
}},
"ItemList": [
{item_list}
],
"ValDtls": {{
"AssVal": "{value_details.base_net_total}",
"CgstVal": "{value_details.total_cgst_amt}",
"SgstVal": "{value_details.total_sgst_amt}",
"IgstVal": "{value_details.total_igst_amt}",
"CesVal": "{value_details.total_cess_amt}",
"Discount": "{value_details.invoice_discount_amt}",
"RndOffAmt": "{value_details.round_off}",
"TotInvVal": "{value_details.base_grand_total}",
"TotInvValFc": "{value_details.grand_total}"
}},
"PayDtls": {{
"Nm": "{payment_details.payee_name}",
"AccDet": "{payment_details.account_no}",
"Mode": "{payment_details.mode_of_payment}",
"FinInsBr": "{payment_details.ifsc_code}",
"PayTerm": "{payment_details.terms}",
"PaidAmt": "{payment_details.paid_amount}",
"PaymtDue": "{payment_details.outstanding_amount}"
}},
"RefDtls": {{
"DocPerdDtls": {{
"InvStDt": "{period_details.start_date}",
"InvEndDt": "{period_details.end_date}"
}},
"PrecDocDtls": {{
"InvNo": "{prev_doc_details.invoice_name}",
"InvDt": "{prev_doc_details.invoice_date}"
}}
}},
"ExpDtls": {{
"ShipBNo": "{export_details.bill_no}",
"ShipBDt": "{export_details.bill_date}",
"Port": "{export_details.port}",
"ForCur": "{export_details.foreign_curr_code}",
"CntCode": "{export_details.country_code}",
"ExpDuty": "{export_details.export_duty}"
}},
"EwbDtls": {{
"TransId": "{eway_bill_details.gstin}",
"TransName": "{eway_bill_details.name}",
"TransMode": "{eway_bill_details.mode_of_transport}",
"Distance": "{eway_bill_details.distance}",
"TransDocNo": "{eway_bill_details.document_name}",
"TransDocDt": "{eway_bill_details.document_date}",
"VehNo": "{eway_bill_details.vehicle_no}",
"VehType": "{eway_bill_details.vehicle_type}"
}}
}}

View File

@@ -0,0 +1,828 @@
{
"Version": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"Irn": {
"type": "string",
"minLength": 64,
"maxLength": 64
},
"TranDtls": {
"type": "object",
"properties": {
"TaxSch": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["GST"]
},
"SupTyp": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"]
},
"RegRev": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"EcmGstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})"
},
"IgstOnIntra": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
}
},
"required": ["TaxSch", "SupTyp"]
},
"DocDtls": {
"type": "object",
"properties": {
"Typ": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"enum": ["INV", "CRN", "DBN"]
},
"No": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$"
},
"Dt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
},
"required": ["Typ", "No", "Dt"]
},
"SellerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 50
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"BuyerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 3,
"maxLength": 15,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pos": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
},
"DispDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ShipDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"maxLength": 15,
"minLength": 3,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ItemList": [
{
"type": "object",
"properties": {
"SlNo": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"PrdDesc": {
"type": "string",
"minLength": 3,
"maxLength": 300
},
"IsServc": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"HsnCd": {
"type": "string",
"minLength": 4,
"maxLength": 8
},
"Barcde": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"Qty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"FreeQty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"Unit": {
"type": "string",
"minLength": 3,
"maxLength": 8
},
"UnitPrice": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.999
},
"TotAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"PreTaxVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"AssAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"GstRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"IgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"SgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"CesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"StateCesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"TotItemVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OrdLineRef": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"OrgCntry": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"PrdSlNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"BchDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 20
},
"ExpDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"WrDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
},
"required": ["Nm"]
},
"AttribDtls": {
"type": "Array",
"Attribute": [
{
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"Val": {
"type": "string",
"minLength": 1,
"maxLength": 100
}
}
}
]
}
},
"required": [
"SlNo",
"IsServc",
"HsnCd",
"UnitPrice",
"TotAmt",
"AssAmt",
"GstRt",
"TotItemVal"
]
}
],
"ValDtls": {
"type": "object",
"properties": {
"AssVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CgstVal": {
"type": "number",
"maximum": 99999999999999.99,
"minimum": 0
},
"SgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"IgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"StCesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"RndOffAmt": {
"type": "number",
"minimum": 0,
"maximum": 99.99
},
"TotInvVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"TotInvValFc": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
},
"required": ["AssVal", "TotInvVal"]
},
"PayDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"AccDet": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"Mode": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"FinInsBr": {
"type": "string",
"minLength": 1,
"maxLength": 11
},
"PayTerm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"PayInstr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrTrn": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"DirDr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrDay": {
"type": "number",
"minimum": 0,
"maximum": 9999
},
"PaidAmt": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"PaymtDue": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
}
},
"RefDtls": {
"type": "object",
"properties": {
"InvRm": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"pattern": "^[0-9A-Za-z/-]{3,100}$"
},
"DocPerdDtls": {
"type": "object",
"properties": {
"InvStDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"InvEndDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
},
"required": ["InvStDt", "InvEndDt"]
},
"PrecDocDtls": {
"type": "object",
"properties": {
"InvNo": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$"
},
"InvDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"OthRefNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
}
},
"required": ["InvNo", "InvDt"]
},
"ContrDtls": {
"type": "object",
"properties": {
"RecAdvRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"RecAdvDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"TendRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ContrRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ExtRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ProjRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"PORefr": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([0-9A-Za-z/-]){1,16}$"
},
"PORefDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
}
}
},
"required": ["InvStDt", "InvEndDt"]
},
"AddlDocDtls": {
"type": "Array",
"AddlDocument": [
{
"type": "object",
"properties": {
"Url": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Docs": {
"type": "string",
"minLength": 3,
"maxLength": 1000
},
"Info": {
"type": "string",
"minLength": 3,
"maxLength": 1000
}
}
}
]
},
"ExpDtls": {
"type": "object",
"properties": {
"ShipBNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"ShipBDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"Port": {
"type": "string",
"minLength": 2,
"maxLength": 10,
"pattern": "^[0-9A-Za-z]{2,10}$"
},
"RefClm": {
"type": "string",
"minLength": 1,
"maxLength": 1
},
"ForCur": {
"type": "string",
"minLength": 3,
"maxLength": 16
},
"CntCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"ExpDuty": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
}
}
},
"EwbDtls": {
"type": "object",
"properties": {
"TransId": {
"type": "string",
"minLength": 15,
"maxLength": 15
},
"TransName": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TransMode": {
"type": "string",
"maxLength": 1,
"minLength": 1,
"enum": ["1", 2, 3, 4]
},
"Distance": {
"type": "number",
"minimum": 1,
"maximum": 9999
},
"TransDocNo": {
"type": "string",
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$"
},
"TransDocDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"VehNo": {
"type": "string",
"minLength": 4,
"maxLength": 20
},
"VehType": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["O", "R"]
}
},
"required": ["Distance"]
},
"required": [
"Version",
"TranDtls",
"DocDtls",
"SellerDtls",
"BuyerDtls",
"ItemList",
"ValDtls"
]
}

View File

@@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"gstin",
"username",
"password"
@@ -30,12 +31,20 @@
"in_list_view": 1,
"label": "Password",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-22 15:10:53.466205",
"modified": "2021-03-22 12:16:56.365616",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",

View File

@@ -1,222 +1,86 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-06-27 15:09:01.318003",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2017-06-27 15:09:01.318003",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"gst_summary",
"column_break_2",
"round_off_gst_values",
"gstin_email_sent_on",
"section_break_4",
"gst_accounts",
"b2c_limit"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "gst_summary",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "GST Summary",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "gst_summary",
"fieldtype": "HTML",
"label": "GST Summary",
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "gstin_email_sent_on",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "GSTIN Email Sent On",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "gstin_email_sent_on",
"fieldtype": "Date",
"label": "GSTIN Email Sent On",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "gst_accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "GST Accounts",
"length": 0,
"no_copy": 0,
"options": "GST Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "gst_accounts",
"fieldtype": "Table",
"label": "GST Accounts",
"options": "GST Account",
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "250000",
"description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
"fieldname": "b2c_limit",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "B2C Limit",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"default": "250000",
"description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
"fieldname": "b2c_limit",
"fieldtype": "Data",
"in_list_view": 1,
"label": "B2C Limit",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Enabling this option will round off individual GST components in all the Invoices",
"fieldname": "round_off_gst_values",
"fieldtype": "Check",
"label": "Round Off GST Values",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-14 08:14:15.375181",
"modified_by": "Administrator",
"module": "Regional",
"name": "GST Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-28 17:19:47.969260",
"modified_by": "Administrator",
"module": "Regional",
"name": "GST Settings",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -172,7 +172,7 @@
</thead>
<tbody>
<tr>
<td><b>(A) {{__("ITC Available (whether in full op part)")}}</b></td>
<td><b>(A) {{__("ITC Available (whether in full or part)")}}</b></td>
<td></td>
<td></td>
<td></td>

View File

@@ -3,148 +3,21 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import json
import frappe
from six import iteritems
from frappe import _
from frappe.model.document import Document
import json
from six import iteritems
from frappe.utils import flt, getdate
from frappe.utils import flt, getdate, cstr
from erpnext.regional.india import state_numbers
class GSTR3BReport(Document):
def before_save(self):
def validate(self):
self.get_data()
def get_data(self):
self.report_dict = {
"gstin": "",
"ret_period": "",
"inward_sup": {
"isup_details": [
{
"ty": "GST",
"intra": 0,
"inter": 0
},
{
"ty": "NONGST",
"inter": 0,
"intra": 0
}
]
},
"sup_details": {
"osup_zero": {
"csamt": 0,
"txval": 0,
"iamt": 0
},
"osup_nil_exmp": {
"txval": 0
},
"osup_det": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"isup_rev": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"osup_nongst": {
"txval": 0,
}
},
"inter_sup": {
"unreg_details": [],
"comp_details": [],
"uin_details": []
},
"itc_elg": {
"itc_avl": [
{
"csamt": 0,
"samt": 0,
"ty": "IMPG",
"camt": 0,
"iamt": 0
},
{
"csamt": 0,
"samt": 0,
"ty": "IMPS",
"camt": 0,
"iamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "ISRC",
"camt": 0,
"iamt": 0
},
{
"ty": "ISD",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "OTH",
"camt": 0,
"iamt": 0
}
],
"itc_rev": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
],
"itc_net": {
"samt": 0,
"csamt": 0,
"camt": 0,
"iamt": 0
},
"itc_inelg": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
]
}
}
self.report_dict = json.loads(get_json('gstr_3b_report_template'))
self.gst_details = self.get_company_gst_details()
self.report_dict["gstin"] = self.gst_details.get("gstin")
@@ -152,23 +25,19 @@ class GSTR3BReport(Document):
self.month_no = get_period(self.month)
self.account_heads = self.get_account_heads()
outward_supply_tax_amounts = self.get_tax_amounts("Sales Invoice")
inward_supply_tax_amounts = self.get_tax_amounts("Purchase Invoice", reverse_charge="Y")
self.get_outward_supply_details("Sales Invoice")
self.set_outward_taxable_supplies()
self.get_outward_supply_details("Purchase Invoice", reverse_charge=True)
self.set_supplies_liable_to_reverse_charge()
itc_details = self.get_itc_details()
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
self.set_itc_details(itc_details)
inter_state_supplies = self.get_inter_state_supplies(self.gst_details.get("gst_state_number"))
self.get_itc_reversal_entries()
inward_nil_exempt = self.get_inward_nil_exempt(self.gst_details.get("gst_state"))
self.set_inter_state_supply(inter_state_supplies)
self.set_inward_nil_exempt(inward_nil_exempt)
self.missing_field_invoices = self.get_missing_field_invoices()
self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt):
@@ -179,182 +48,95 @@ class GSTR3BReport(Document):
self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2)
def set_itc_details(self, itc_details):
itc_type_map = {
itc_eligible_type_map = {
'IMPG': 'Import Of Capital Goods',
'IMPS': 'Import Of Service',
'ISRC': 'ITC on Reverse Charge',
'ISD': 'Input Service Distributor',
'OTH': 'All Other ITC'
}
itc_ineligible_map = {
'RUL': 'Ineligible As Per Section 17(5)',
'OTH': 'Ineligible Others'
}
net_itc = self.report_dict["itc_elg"]["itc_net"]
for d in self.report_dict["itc_elg"]["itc_avl"]:
itc_type = itc_type_map.get(d["ty"])
if d["ty"] == 'ISRC':
reverse_charge = ["Y"]
itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas']
else:
gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
reverse_charge = ["N", "Y"]
for account_head in self.account_heads:
for category in gst_category:
for charge_type in reverse_charge:
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
itc_type = itc_eligible_type_map.get(d["ty"])
for key in ['iamt', 'camt', 'samt', 'csamt']:
d[key] = flt(itc_details.get(itc_type, {}).get(key))
net_itc[key] += flt(d[key], 2)
for account_head in self.account_heads:
itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1]
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2)
for d in self.report_dict["itc_elg"]["itc_inelg"]:
itc_type = itc_ineligible_map.get(d["ty"])
for key in ['iamt', 'camt', 'samt', 'csamt']:
d[key] = flt(itc_details.get(itc_type, {}).get(key))
def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"):
def get_itc_reversal_entries(self):
reversal_entries = frappe.db.sql("""
SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
where j.docstatus = 1
and j.is_opening = 'No'
and ja.parent = j.name
and j.voucher_type = 'Reversal Of ITC'
and month(j.posting_date) = %s and year(j.posting_date) = %s
and j.company = %s and j.company_gstin = %s
GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company,
self.gst_details.get("gstin")), as_dict=1)
account_map = {
'sgst_account': 'samt',
'cess_account': 'csamt',
'cgst_account': 'camt',
'igst_account': 'iamt'
}
net_itc = self.report_dict["itc_elg"]["itc_net"]
txval = 0
total_taxable_value = self.get_total_taxable_value(doctype, reverse_charge)
for entry in reversal_entries:
if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules':
index = 0
else:
index = 1
for gst_category in gst_category_list:
txval += total_taxable_value.get(gst_category,0)
for account_head in self.account_heads:
for account_type, account_name in iteritems(account_head):
if account_map.get(account_type) in self.report_dict.get(supply_type).get(supply_category):
self.report_dict[supply_type][supply_category][account_map.get(account_type)] += \
flt(tax_details.get((account_name, gst_category), {}).get("amount"), 2)
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
def set_inter_state_supply(self, inter_state_supply):
osup_det = self.report_dict["sup_details"]["osup_det"]
for key, value in iteritems(inter_state_supply):
if key[0] == "Unregistered":
self.report_dict["inter_sup"]["unreg_details"].append(value)
if key[0] == "Registered Composition":
self.report_dict["inter_sup"]["comp_details"].append(value)
if key[0] == "UIN Holders":
self.report_dict["inter_sup"]["uin_details"].append(value)
def get_total_taxable_value(self, doctype, reverse_charge):
return frappe._dict(frappe.db.sql("""
select gst_category, sum(net_total) as total
from `tab{doctype}`
where docstatus = 1 and month(posting_date) = %s
and year(posting_date) = %s and reverse_charge = %s
and company = %s and company_gstin = %s
group by gst_category
""" #nosec
.format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin"))))
for key in ['camt', 'samt', 'iamt', 'csamt']:
if entry.account in self.account_heads.get(key):
self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount)
net_itc[key] -= flt(entry.amount)
def get_itc_details(self):
itc_amount = frappe.db.sql("""
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
t.account_head, s.eligibility_for_itc, s.reverse_charge
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
where s.docstatus = 1 and t.parent = s.name
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
and s.company_gstin = %s
group by t.account_head, s.gst_category, s.eligibility_for_itc
""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
itc_amounts = frappe.db.sql("""
SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax,
sum(itc_central_tax) as itc_central_tax,
sum(itc_state_tax) as itc_state_tax,
sum(itc_cess_amount) as itc_cess_amount
FROM `tabPurchase Invoice`
WHERE docstatus = 1
and is_opening = 'No'
and month(posting_date) = %s and year(posting_date) = %s and company = %s
and company_gstin = %s
GROUP BY eligibility_for_itc
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
itc_details = {}
for d in itc_amount:
itc_details.setdefault((d.gst_category, d.eligibility_for_itc, d.reverse_charge, d.account_head),{
"amount": d.tax_amount
for d in itc_amounts:
itc_details.setdefault(d.eligibility_for_itc, {
'iamt': d.itc_integrated_tax,
'camt': d.itc_central_tax,
'samt': d.itc_state_tax,
'csamt': d.itc_cess_amount
})
return itc_details
def get_nil_rated_supply_value(self):
return frappe.db.sql("""
select sum(i.base_amount) as total from
`tabSales Invoice Item` i, `tabSales Invoice` s
where s.docstatus = 1 and i.parent = s.name and i.is_nil_exempt = 1
and month(s.posting_date) = %s and year(s.posting_date) = %s
and s.company = %s and s.company_gstin = %s""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)[0].total
def get_inter_state_supplies(self, state_number):
inter_state_supply_tax = frappe.db.sql(""" select t.account_head, t.tax_amount_after_discount_amount as tax_amount,
s.name, s.net_total, s.place_of_supply, s.gst_category from `tabSales Invoice` s, `tabSales Taxes and Charges` t
where t.parent = s.name and s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s
and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders')
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inter_state_supply_tax_mapping = {}
inter_state_supply_details = {}
for d in inter_state_supply_tax:
inter_state_supply_tax_mapping.setdefault(d.name, {
'place_of_supply': d.place_of_supply,
'taxable_value': d.net_total,
'gst_category': d.gst_category,
'camt': 0.0,
'samt': 0.0,
'iamt': 0.0,
'csamt': 0.0
})
if d.account_head in [a.cgst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount
if d.account_head in [a.sgst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount
if d.account_head in [a.igst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount
if d.account_head in [a.cess_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount
for key, value in iteritems(inter_state_supply_tax_mapping):
if value.get('place_of_supply'):
osup_det = self.report_dict["sup_details"]["osup_det"]
osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2)
osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2)
osup_det["camt"] = flt(osup_det["camt"] + value['camt'], 2)
osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2)
osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2)
if state_number != value.get('place_of_supply').split("-")[0]:
inter_state_supply_details.setdefault((value.get('gst_category'), value.get('place_of_supply')), {
"txval": 0.0,
"pos": value.get('place_of_supply').split("-")[0],
"iamt": 0.0
})
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['txval'] += value['taxable_value']
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['iamt'] += value['iamt']
return inter_state_supply_details
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt = frappe.db.sql("""
SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
and p.gst_category != 'Registered Composition'
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
"gst": {
@@ -380,37 +162,190 @@ class GSTR3BReport(Document):
return inward_nil_exempt_details
def get_tax_amounts(self, doctype, reverse_charge="N"):
def get_outward_supply_details(self, doctype, reverse_charge=None):
self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge)
self.get_outward_items(doctype)
self.get_outward_tax_details(doctype)
def get_outward_tax_invoices(self, doctype, reverse_charge=None):
self.invoices = []
self.invoice_detail_map = {}
condition = ''
if reverse_charge:
condition += "AND reverse_charge = 'Y'"
invoice_details = frappe.db.sql("""
SELECT
name, gst_category, export_type, place_of_supply
FROM
`tab{doctype}`
WHERE
docstatus = 1
AND month(posting_date) = %s
AND year(posting_date) = %s
AND company = %s
AND company_gstin = %s
AND is_opening = 'No'
{reverse_charge}
ORDER BY name
""".format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year,
self.company, self.gst_details.get("gstin")), as_dict=1)
for d in invoice_details:
self.invoice_detail_map.setdefault(d.name, d)
self.invoices.append(d.name)
def get_outward_items(self, doctype):
self.invoice_items = frappe._dict()
self.is_nil_exempt = []
self.is_non_gst = []
item_details = frappe.db.sql("""
SELECT
item_code, parent, taxable_value, base_net_amount, item_tax_rate,
is_nil_exempt, is_non_gst
FROM
`tab%s Item`
WHERE parent in (%s)
""" % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
for d in item_details:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
if i.item_code == d.item_code and i.parent == d.parent))
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
self.is_nil_exempt.append(d.item_code)
if d.is_non_gst and d.item_code not in self.is_non_gst:
self.is_non_gst.append(d.item_code)
def get_outward_tax_details(self, doctype):
if doctype == "Sales Invoice":
tax_template = 'Sales Taxes and Charges'
elif doctype == "Purchase Invoice":
tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql("""
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
from `tab{doctype}` s , `tab{template}` t
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
and s.company_gstin = %s
group by t.account_head, s.gst_category
""" #nosec
.format(doctype=doctype, template=tax_template),
(reverse_charge, self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
self.items_based_on_tax_rate = {}
self.invoice_cess = frappe._dict()
self.cgst_sgst_invoices = []
tax_details = {}
tax_details = frappe.db.sql("""
SELECT
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
FROM `tab%s`
WHERE
parenttype = %s and docstatus = 1
and parent in (%s)
ORDER BY account_head
""" % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))),
tuple([doctype] + list(self.invoices)))
for d in tax_amounts:
tax_details.setdefault(
(d.account_head,d.gst_category),{
"amount": d.get("tax_amount"),
}
)
for parent, account, item_wise_tax_detail, tax_amount in tax_details:
if account in self.account_heads.get('csamt'):
self.invoice_cess.setdefault(parent, tax_amount)
else:
if item_wise_tax_detail:
try:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
cgst_or_sgst = False
if account in self.account_heads.get('camt') \
or account in self.account_heads.get('samt'):
cgst_or_sgst = True
return tax_details
for item_code, tax_amounts in item_wise_tax_detail.items():
if not (cgst_or_sgst or account in self.account_heads.get('iamt') or
(item_code in self.is_non_gst + self.is_nil_exempt)):
continue
tax_rate = tax_amounts[0]
if tax_rate:
if cgst_or_sgst:
tax_rate *= 2
if parent not in self.cgst_sgst_invoices:
self.cgst_sgst_invoices.append(parent)
rate_based_dict = self.items_based_on_tax_rate\
.setdefault(parent, {}).setdefault(tax_rate, [])
if item_code not in rate_based_dict:
rate_based_dict.append(item_code)
except ValueError:
continue
# Build itemised tax for export invoices, nil and exempted where tax table is blank
for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type')\
== "Without Payment of Tax"):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def set_outward_taxable_supplies(self):
inter_state_supply_details = {}
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
if item_code in self.is_nil_exempt:
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
elif item_code in self.is_non_gst:
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
elif rate == 0:
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
#self.report_dict['sup_details']['osup_zero'][key] += tax_amount
else:
if inv in self.cgst_sgst_invoices:
tax_rate = rate/2
self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
else:
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory')
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply:
inter_state_supply_details.set_default((gst_category, place_of_supply), {
"txval": 0.0,
"pos": value.get('place_of_supply').split("-")[0],
"iamt": 0.0
})
inter_state_supply_details[(gst_category, place_of_supply)]['taxval'] += taxable_value
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
self.set_inter_state_supply(inter_state_supply_details)
def set_supplies_liable_to_reverse_charge(self):
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
if inv in self.cgst_sgst_invoices:
tax_rate = rate/2
self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
else:
self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
def set_inter_state_supply(self, inter_state_supply):
for key, value in iteritems(inter_state_supply):
if key[0] == "Unregistered":
self.report_dict["inter_sup"]["unreg_details"].append(value)
if key[0] == "Registered Composition":
self.report_dict["inter_sup"]["comp_details"].append(value)
if key[0] == "UIN Holders":
self.report_dict["inter_sup"]["uin_details"].append(value)
def get_company_gst_details(self):
gst_details = frappe.get_all("Address",
fields=["gstin", "gst_state", "gst_state_number"],
filters={
@@ -423,20 +358,28 @@ class GSTR3BReport(Document):
frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address))
def get_account_heads(self):
account_map = {
'sgst_account': 'samt',
'cess_account': 'csamt',
'cgst_account': 'camt',
'igst_account': 'iamt'
}
account_heads = frappe.get_all("GST Account",
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"],
filters={
"company":self.company
})
account_heads = {}
gst_settings_accounts = frappe.get_all("GST Account",
filters={'company': self.company, 'is_reverse_charge_account': 0},
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
if account_heads:
return account_heads
else:
frappe.throw(_("Please set account heads in GST Settings for Compnay {0}").format(self.company))
if not gst_settings_accounts:
frappe.throw(_("Please set GST Accounts in GST Settings"))
for d in gst_settings_accounts:
for acc, val in d.items():
account_heads.setdefault(account_map.get(acc), []).append(val)
return account_heads
def get_missing_field_invoices(self):
missing_field_invoices = []
for doctype in ["Sales Invoice", "Purchase Invoice"]:
@@ -448,26 +391,32 @@ class GSTR3BReport(Document):
party_type = 'Supplier'
party = 'supplier'
docnames = frappe.db.sql("""
select t1.name from `tab{doctype}` t1, `tab{party_type}` t2
where t1.docstatus = 1 and month(t1.posting_date) = %s and year(t1.posting_date) = %s
docnames = frappe.db.sql(
"""
SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2
WHERE t1.docstatus = 1 and t1.is_opening = 'No'
and month(t1.posting_date) = %s and year(t1.posting_date) = %s
and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and
t2.gst_category != 'Overseas'
""".format(doctype = doctype, party_type = party_type, party=party), (self.month_no, self.year, self.company), as_dict=1) #nosec
""".format(doctype = doctype, party_type = party_type,
party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec
for d in docnames:
missing_field_invoices.append(d.name)
return ",".join(missing_field_invoices)
def get_state_code(state):
def get_json(template):
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_state_code(state):
state_code = state_numbers.get(state)
return state_code
def get_period(month, year=None):
month_no = {
"January": 1,
"February": 2,
@@ -491,13 +440,11 @@ def get_period(month, year=None):
@frappe.whitelist()
def view_report(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
return json.loads(json_data)
@frappe.whitelist()
def make_json(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
file_name = "GST3B.json"
frappe.local.response.filename = file_name

View File

@@ -0,0 +1,127 @@
{
"gstin": "",
"ret_period": "",
"inward_sup": {
"isup_details": [
{
"ty": "GST",
"intra": 0,
"inter": 0
},
{
"ty": "NONGST",
"inter": 0,
"intra": 0
}
]
},
"sup_details": {
"osup_zero": {
"csamt": 0,
"txval": 0,
"iamt": 0
},
"osup_nil_exmp": {
"txval": 0
},
"osup_det": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"isup_rev": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"osup_nongst": {
"txval": 0
}
},
"inter_sup": {
"unreg_details": [],
"comp_details": [],
"uin_details": []
},
"itc_elg": {
"itc_avl": [
{
"csamt": 0,
"samt": 0,
"ty": "IMPG",
"camt": 0,
"iamt": 0
},
{
"csamt": 0,
"samt": 0,
"ty": "IMPS",
"camt": 0,
"iamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "ISRC",
"camt": 0,
"iamt": 0
},
{
"ty": "ISD",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "OTH",
"camt": 0,
"iamt": 0
}
],
"itc_rev": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
],
"itc_net": {
"samt": 0,
"csamt": 0,
"camt": 0,
"iamt": 0
},
"itc_inelg": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
]
}
}

View File

@@ -14,8 +14,20 @@ import json
test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
class TestGSTR3BReport(unittest.TestCase):
def test_gstr_3b_report(self):
def setUp(self):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
make_company()
make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
set_account_heads()
make_customers()
make_suppliers()
def test_gstr_3b_report(self):
month_number_mapping = {
1: "January",
2: "February",
@@ -31,17 +43,6 @@ class TestGSTR3BReport(unittest.TestCase):
12: "December"
}
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
make_company()
make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
set_account_heads()
make_customers()
make_suppliers()
make_sales_invoice()
create_purchase_invoices()
@@ -67,6 +68,42 @@ class TestGSTR3BReport(unittest.TestCase):
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
def test_gst_rounding(self):
gst_settings = frappe.get_doc('GST Settings')
gst_settings.round_off_gst_values = 1
gst_settings.save()
current_country = frappe.flags.country
frappe.flags.country = 'India'
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
currency = 'INR',
warehouse = 'Finished Goods - _GST',
debit_to = 'Debtors - _GST',
income_account = 'Sales - _GST',
expense_account = 'Cost of Goods Sold - _GST',
cost_center = 'Main - _GST',
rate=216,
do_not_save=1
)
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
})
si.save()
# Check for 39 instead of 38.88
self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39)
frappe.flags.country = current_country
gst_settings.round_off_gst_values = 1
gst_settings.save()
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
@@ -145,7 +182,6 @@ def make_sales_invoice():
si3.submit()
def create_purchase_invoices():
pi = make_purchase_invoice(
company="_Test Company GST",
supplier = '_Test Registered Supplier',
@@ -193,7 +229,6 @@ def create_purchase_invoices():
pi1.submit()
def make_suppliers():
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
"supplier_group": "_Test Supplier Group",
@@ -257,7 +292,6 @@ def make_suppliers():
address.save()
def make_customers():
if not frappe.db.exists("Customer", "_Test GST Customer"):
frappe.get_doc({
"customer_group": "_Test Customer Group",
@@ -354,9 +388,9 @@ def make_customers():
address.save()
def make_company():
if frappe.db.exists("Company", "_Test Company GST"):
return
company = frappe.new_doc("Company")
company.company_name = "_Test Company GST"
company.abbr = "_GST"
@@ -388,7 +422,6 @@ def make_company():
address.save()
def set_account_heads():
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(

View File

@@ -1,13 +1,13 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
const { message } = await frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const einvoicing_enabled = cint(message.enable);
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
});
const invoice_eligible = res.message;
if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
if (!invoice_eligible) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@@ -110,45 +110,25 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Cancel E-Way Bill'),
fields: fields,
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.');
frappe.msgprint({
title: __('Update E-Way Bill Cancelled Status?'),
message: message,
indicator: 'orange',
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: {
doctype,
docname: name,
eway_bill: ewaybill,
reason: data.reason.split('-')[0],
remark: data.remark
},
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
callback: () => frm.reload_doc()
});
},
primary_action_label: __('Submit')
primary_action_label: __('Yes')
});
d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}

View File

@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import os
import re
import six
import jwt
import sys
import json
@@ -16,16 +17,38 @@ from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, getdate, get_link_to_form
def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
invalid_doctype = doc.doctype != 'Sales Invoice'
@frappe.whitelist()
def validate_eligibility(doc):
if isinstance(doc, six.string_types):
doc = json.loads(doc)
invalid_doctype = doc.get('doctype') != 'Sales Invoice'
if invalid_doctype:
return False
einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
if not einvoicing_enabled:
return False
if getdate(doc.get('posting_date')) < getdate('2021-04-01'):
return False
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
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 not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
return False
return True
def validate_einvoice_fields(doc):
invoice_eligible = validate_eligibility(doc)
if not invoice_eligible:
return
if doc.docstatus == 0 and doc._action == 'save':
@@ -86,35 +109,39 @@ def get_doc_details(invoice):
invoice_date=invoice_date
))
def get_party_details(address_name):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
if (not d.gstin
or not d.city
or not d.pincode
or not d.address_title
or not d.address_line1
or not d.gst_state_number):
def validate_address_fields(address, is_shipping_address):
if ((not address.gstin and not is_shipping_address)
or not address.city
or not address.pincode
or not address.address_title
or not address.address_line1
or not address.gst_state_number):
frappe.throw(
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
title=_('Missing Address Fields')
)
if d.gst_state_number == 97:
# according to einvoice standard
pincode = 999999
def get_party_details(address_name, is_shipping_address=False):
addr = frappe.get_doc('Address', address_name)
return frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title,
location=d.city, pincode=d.pincode,
state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
validate_address_fields(addr, is_shipping_address)
if addr.gst_state_number == 97:
# according to einvoice standard
addr.pincode = 999999
party_address_details = frappe._dict(dict(
legal_name=sanitize_for_json(addr.address_title),
location=sanitize_for_json(addr.city),
pincode=addr.pincode, gstin=addr.gstin,
state_code=addr.gst_state_number,
address_line1=sanitize_for_json(addr.address_line1),
address_line2=sanitize_for_json(addr.address_line2)
))
return party_address_details
def get_gstin_details(gstin):
if not hasattr(frappe.local, 'gstin_cache'):
frappe.local.gstin_cache = {}
@@ -163,10 +190,15 @@ def get_item_list(invoice):
item.description = json.dumps(d.item_name)[1:-1]
item.qty = abs(item.qty)
item.discount_amount = 0
item.unit_rate = abs(item.base_net_amount / item.qty)
item.gross_amount = abs(item.base_net_amount)
item.taxable_value = abs(item.base_net_amount)
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.taxable_value = abs(item.taxable_value)
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
@@ -196,12 +228,14 @@ def update_item_taxes(invoice, item):
item[attr] = 0
for t in invoice.taxes:
# this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts_list:
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
if is_applicable:
# this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
item_tax_amount = (item_tax_rate / 100) * item.taxable_value
if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1]
@@ -222,10 +256,14 @@ def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total)
# 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(invoice.base_net_total)
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
@@ -246,7 +284,11 @@ def update_invoice_taxes(invoice, invoice_value_details):
invoice_value_details.total_igst_amt = 0
invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0
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
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
@@ -254,12 +296,26 @@ def update_invoice_taxes(invoice, invoice_value_details):
for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']:
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
invoice_value_details.total_other_charges += abs(tax_amount)
return invoice_value_details
def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
prev_row_id = cint(tax_row.row_id) - 1
if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
if tax_row.charge_type == 'On Previous Row Amount':
amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
invoice_value_details.total_other_charges -= abs(amount)
considered_rows.append(prev_row_id)
if tax_row.charge_type == 'On Previous Row Total':
amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
invoice_value_details.total_other_charges -= abs(amount)
considered_rows.append(prev_row_id)
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
@@ -272,6 +328,10 @@ def get_payment_details(invoice):
))
def get_return_doc_reference(invoice):
if not invoice.return_against:
frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
.format(frappe.bold('Return Against')), title=_('Missing Field'))
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
@@ -279,7 +339,11 @@ def get_return_doc_reference(invoice):
def get_eway_bill_details(invoice):
if invoice.is_return:
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
title=_('Invalid Fields'))
if not invoice.distance:
frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@@ -297,9 +361,15 @@ def get_eway_bill_details(invoice):
def validate_mandatory_fields(invoice):
if not invoice.company_address:
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
frappe.throw(
_('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
title=_('Missing Fields')
)
if not invoice.customer_address:
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
frappe.throw(
_('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
title=_('Missing Fields')
)
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
@@ -311,6 +381,39 @@ def validate_mandatory_fields(invoice):
title=_('Missing Fields')
)
def validate_totals(einvoice):
item_list = einvoice['ItemList']
value_details = einvoice['ValDtls']
total_item_ass_value = 0
total_item_cgst_value = 0
total_item_sgst_value = 0
total_item_igst_value = 0
total_item_value = 0
for item in item_list:
total_item_ass_value += flt(item['AssAmt'])
total_item_cgst_value += flt(item['CgstAmt'])
total_item_sgst_value += flt(item['SgstAmt'])
total_item_igst_value += flt(item['IgstAmt'])
total_item_value += flt(item['TotItemVal'])
if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
calculated_invoice_value = \
flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ flt(value_details['OthChrg']) - flt(value_details['Discount'])
if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
@@ -326,16 +429,23 @@ def make_einvoice(invoice):
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
place_of_supply = place_of_supply[:2]
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
if place_of_supply:
place_of_supply = place_of_supply.split('-')[0]
else:
place_of_supply = invoice.billing_address_gstin[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
seller_details.update(dict(legal_name=invoice.company))
buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
shipping_details = get_party_details(invoice.shipping_address_name)
shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
@@ -343,7 +453,7 @@ def make_einvoice(invoice):
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter:
if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
@@ -356,71 +466,86 @@ def make_einvoice(invoice):
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
einvoice = json.loads(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
if errors:
message = "\n".join([
"E Invoice: ", json.dumps(einvoice, indent=4),
"-" * 50,
"Errors: ", json.dumps(errors, indent=4)
])
frappe.log_error(title="E Invoice Validation Failed", message=message)
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
try:
einvoice = safe_json_load(einvoice)
einvoice = santize_einvoice_fields(einvoice)
except Exception:
show_link_to_error_log(invoice, einvoice)
validate_totals(einvoice)
return einvoice
def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
if not value or value == "None":
# remove keys with empty values
einvoice.pop(fieldname, None)
continue
def show_link_to_error_log(invoice, einvoice):
err_log = log_error(einvoice)
link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
frappe.throw(
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
invoice.name, link_to_error_log),
title=_('E Invoice Creation Failed')
)
value_type = field_validation.get("type").lower()
if value_type in ['object', 'array']:
child_validations = field_validation.get('properties')
def log_error(data=None):
if isinstance(data, six.string_types):
data = json.loads(data)
if isinstance(value, list):
for d in value:
validate_einvoice(child_validations, d, errors)
if not d:
# remove empty dicts
einvoice.pop(fieldname, None)
seperator = "--" * 50
err_tb = traceback.format_exc()
err_msg = str(sys.exc_info()[1])
data = json.dumps(data, indent=4)
message = "\n".join([
"Error", err_msg, seperator,
"Data:", data, seperator,
"Exception:", err_tb
])
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
def santize_einvoice_fields(einvoice):
int_fields = ["Pin","Distance","CrDay"]
float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
copy = einvoice.copy()
for key, value in copy.items():
if isinstance(value, list):
for idx, d in enumerate(value):
santized_dict = santize_einvoice_fields(d)
if santized_dict:
einvoice[key][idx] = santized_dict
else:
einvoice[key].pop(idx)
if not einvoice[key]:
einvoice.pop(key, None)
elif isinstance(value, dict):
santized_dict = santize_einvoice_fields(value)
if santized_dict:
einvoice[key] = santized_dict
else:
validate_einvoice(child_validations, value, errors)
if not value:
# remove empty dicts
einvoice.pop(fieldname, None)
continue
einvoice.pop(key, None)
# convert to int or str
if value_type == 'string':
einvoice[fieldname] = str(value)
elif value_type == 'number':
is_integer = '.' not in str(field_validation.get('maximum'))
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
value = einvoice[fieldname]
elif not value or value == "None":
einvoice.pop(key, None)
max_length = field_validation.get('maxLength')
minimum = flt(field_validation.get('minimum'))
maximum = flt(field_validation.get('maximum'))
pattern_str = field_validation.get('pattern')
pattern = re.compile(pattern_str or '')
elif key in float_fields:
einvoice[key] = flt(value, 2)
label = field_validation.get('description') or fieldname
elif key in int_fields:
einvoice[key] = cint(value)
if value_type == 'string' and len(value) > max_length:
errors.append(_('{} should not exceed {} characters').format(label, max_length))
if value_type == 'number' and (value > maximum or value < minimum):
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
if pattern_str and not pattern.match(value):
errors.append(field_validation.get('validationMsg'))
return einvoice
return errors
def safe_json_load(json_string):
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
try:
return json.loads(json_string)
except JSONDecodeError as e:
# print a snippet of 40 characters around the location where error occured
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))
class RequestFailed(Exception): pass
@@ -446,15 +571,17 @@ class GSPConnector():
def get_credentials(self):
if self.invoice:
gstin = self.get_seller_gstin()
if not self.e_invoice_settings.enable:
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
if credentials_for_gstin:
credentials = credentials_for_gstin[0]
else:
frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
return credentials
def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
if not gstin:
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
return gstin
@@ -502,7 +629,7 @@ class GSPConnector():
self.e_invoice_settings.reload()
except Exception:
self.log_error(res)
log_error(res)
self.raise_error(True)
def get_headers(self):
@@ -524,14 +651,14 @@ class GSPConnector():
if res.get('success'):
return res.get('result')
else:
self.log_error(res)
log_error(res)
raise RequestFailed
except RequestFailed:
self.raise_error()
except Exception:
self.log_error()
log_error()
self.raise_error(True)
@staticmethod
def get_gstin_details(gstin):
@@ -576,7 +703,7 @@ class GSPConnector():
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
log_error(data)
self.raise_error(True)
def get_irn_details(self, irn):
@@ -595,7 +722,7 @@ class GSPConnector():
self.raise_error(errors=errors)
except Exception:
self.log_error()
log_error()
self.raise_error(True)
def cancel_irn(self, irn, reason, remark):
@@ -608,7 +735,7 @@ class GSPConnector():
try:
res = self.make_request('post', self.cancel_irn_url, headers, data)
if res.get('success'):
if res.get('success') or '9999' in res.get('message'):
self.invoice.irn_cancelled = 1
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
@@ -625,7 +752,7 @@ class GSPConnector():
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
log_error(data)
self.raise_error(True)
def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs)
@@ -665,7 +792,7 @@ class GSPConnector():
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
log_error(data)
self.raise_error(True)
def cancel_eway_bill(self, eway_bill, reason, remark):
@@ -697,7 +824,7 @@ class GSPConnector():
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
log_error(data)
self.raise_error(True)
def sanitize_error_message(self, message):
@@ -723,22 +850,6 @@ class GSPConnector():
return errors
def log_error(self, data={}):
if not isinstance(data, dict):
data = json.loads(data)
seperator = "--" * 50
err_tb = traceback.format_exc()
err_msg = str(sys.exc_info()[1])
data = json.dumps(data, indent=4)
message = "\n".join([
"Error", err_msg, seperator,
"Data:", data, seperator,
"Exception:", err_tb
])
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
def raise_error(self, raise_exception=False, errors=[]):
title = _('E Invoice Request Failed')
if errors:
@@ -758,6 +869,8 @@ class GSPConnector():
self.invoice.irn = res.get('Irn')
self.invoice.ewaybill = res.get('EwbNo')
self.invoice.ack_no = res.get('AckNo')
self.invoice.ack_date = res.get('AckDt')
self.invoice.signed_einvoice = dec_signed_invoice
self.invoice.signed_qr_code = res.get('SignedQRCode')
@@ -795,6 +908,11 @@ class GSPConnector():
self.invoice.flags.ignore_validate = True
self.invoice.save()
def sanitize_for_json(string):
"""Escape JSON specific characters from a string."""
# json.dumps adds double-quotes to the string. Indexing to remove them.
return json.dumps(string)[1:-1]
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
@@ -817,5 +935,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
# TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
# update cancelled status only, to be able to cancel irn next
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)

View File

@@ -0,0 +1,578 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import re
import jwt
import json
import base64
import frappe
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, AES
from Crypto.Util.Padding import pad, unpad
from frappe.model.document import Document
from frappe import _, get_module_path, scrub
from erpnext.regional.india.utils import get_gst_accounts
from frappe.integrations.utils import make_post_request, make_get_request
from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime
def validate_einvoice_fields(doc):
e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable")
if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not e_invoice_enabled: return
if doc.docstatus == 0 and doc._action == 'save' and doc.irn:
frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed"))
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN"))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed"))
def get_einv_credentials(for_token=False):
creds = frappe.get_doc("E Invoice Settings")
if not for_token and (not creds.token_expiry or time_diff_in_seconds(now_datetime(), creds.token_expiry) > 5.0):
fetch_token()
creds.load_from_db()
return creds
def rsa_encrypt(msg, key):
if not (isinstance(msg, bytes) or isinstance(msg, bytearray)):
msg = str.encode(msg)
rsa_pub_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(rsa_pub_key)
enc_msg = cipher.encrypt(msg)
b64_enc_msg = base64.b64encode(enc_msg)
return b64_enc_msg.decode()
def aes_decrypt(enc_msg, key):
encode_as_b64 = True
if not (isinstance(key, bytes) or isinstance(key, bytearray)):
key = base64.b64decode(key)
encode_as_b64 = False
cipher = AES.new(key, AES.MODE_ECB)
b64_enc_msg = base64.b64decode(enc_msg)
msg_bytes = cipher.decrypt(b64_enc_msg)
msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding
if encode_as_b64:
msg_bytes = base64.b64encode(msg_bytes)
return msg_bytes.decode()
def aes_encrypt(msg, key):
if not (isinstance(key, bytes) or isinstance(key, bytearray)):
key = base64.b64decode(key)
cipher = AES.new(key, AES.MODE_ECB)
bytes_msg = str.encode(msg)
padded_bytes_msg = pad(bytes_msg, AES.block_size)
enc_msg = cipher.encrypt(padded_bytes_msg)
b64_enc_msg = base64.b64encode(enc_msg)
return b64_enc_msg.decode()
def jwt_decrypt(token):
return jwt.decode(token, verify=False)
def get_header(creds):
headers = { 'content-type': 'application/json' }
headers.update(dict(client_id=creds.client_id, client_secret=creds.client_secret, user_name=creds.username))
headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token))
return headers
@frappe.whitelist()
def fetch_token():
einv_creds = get_einv_credentials(for_token=True)
endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth'
headers = { 'content-type': 'application/json' }
headers.update(dict(client_id=einv_creds.client_id, client_secret=einv_creds.client_secret))
payload = dict(UserName=einv_creds.username, ForceRefreshAccessToken=bool(einv_creds.auto_refresh_token))
appkey = bytearray(os.urandom(32))
enc_appkey = rsa_encrypt(appkey, einv_creds.public_key)
password = einv_creds.get_password(fieldname='password')
enc_password = rsa_encrypt(password, einv_creds.public_key)
payload.update(dict(Password=enc_password, AppKey=enc_appkey))
res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload }))
handle_err_response(res)
auth_token, token_expiry, sek = extract_token_and_sek(res, appkey)
einv_creds.auth_token = auth_token
einv_creds.token_expiry = get_datetime(token_expiry)
einv_creds.sek = sek
einv_creds.save()
def extract_token_and_sek(response, appkey):
data = response.get('Data')
auth_token = data.get('AuthToken')
token_expiry = data.get('TokenExpiry')
enc_sek = data.get('Sek')
sek = aes_decrypt(enc_sek, appkey)
return auth_token, token_expiry, sek
def attach_signed_json(invoice, data):
f = frappe.get_doc({
"doctype": "File",
"file_name": invoice.name + "e_invoice.json",
"attached_to_doctype": invoice.doctype,
"attached_to_name": invoice.name,
"content": json.dumps(data),
"is_private": True
}).insert()
def get_gstin_details(gstin):
einv_creds = get_einv_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin)
headers = get_header(einv_creds)
res = make_get_request(endpoint, headers=headers)
handle_err_response(res)
enc_json = res.get('Data')
json_str = aes_decrypt(enc_json, einv_creds.sek)
data = json.loads(json_str)
return data
@frappe.whitelist()
def generate_irn(doctype, name):
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice'
einv_creds = get_einv_credentials()
headers = get_header(einv_creds)
invoice = frappe.get_doc(doctype, name)
e_invoice = make_e_invoice(invoice)
enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek)
payload = dict(Data=enc_e_invoice_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
res = handle_err_response(res)
enc_json = res.get('Data')
json_str = aes_decrypt(enc_json, einv_creds.sek)
data = json.loads(json_str)
handle_irn_response(data)
attach_signed_json(invoice, data['DecryptedSignedInvoice'])
return data
def get_irn_details(irn):
einv_creds = get_einv_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn)
headers = get_header(einv_creds)
res = make_get_request(endpoint, headers=headers)
handle_err_response(res)
# enc_json = res.get('Data')
# json_str = aes_decrypt(enc_json, einv_creds.sek)
# data = json.loads(json_str)
# handle_irn_response(data)
return res
@frappe.whitelist()
def cancel_irn(irn, reason, remark=''):
einv_creds = get_einv_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel'
headers = get_header(einv_creds)
cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark))
enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek)
payload = dict(Data=enc_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
handle_err_response(res)
return res
@frappe.whitelist()
def cancel_eway_bill(eway_bill, reason, remark=''):
einv_creds = get_einv_credentials()
endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi'
headers = get_header(einv_creds)
cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark))
enc_json = aes_encrypt(cancel_eway_bill_json, einv_creds.sek)
payload = dict(action="CANEWB", Data=enc_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
handle_err_response(res)
return res
def handle_irn_response(data):
enc_signed_invoice = data['SignedInvoice']
enc_signed_qr_code = data['SignedQRCode']
signed_invoice = jwt_decrypt(enc_signed_invoice)['data']
signed_qr_code = jwt_decrypt(enc_signed_qr_code)['data']
data['DecryptedSignedInvoice'] = json.loads(signed_invoice)
data['DecryptedSignedQRCode'] = json.loads(signed_qr_code)
def handle_err_response(response):
if response.get('Status') == 0:
err_details = response.get('ErrorDetails')
print(response)
err_msg = ""
for d in err_details:
err_code = d.get('ErrorCode')
if err_code == '2150':
irn = [d['Desc']['Irn'] for d in response.get('InfoDtls') if d['InfCd'] == 'DUPIRN']
response = get_irn_details(irn[0])
return response
err_msg += d.get('ErrorMessage')
err_msg += "<br>"
frappe.throw(_(err_msg), title=_('API Request Failed'))
return response
def read_json(name):
file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_trans_details(invoice):
supply_type = ''
if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
if not supply_type:
return _('Invalid invoice transaction category.')
return frappe._dict(dict(
tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge
))
def get_doc_details(invoice):
if invoice.doctype == 'Purchase Invoice' and invoice.is_return:
invoice_type = 'DBN'
else:
invoice_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
return frappe._dict(dict(
invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date
))
def get_party_gstin_details(party_address):
address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0]
gstin = address.get('gstin')
gstin_details = get_gstin_details(gstin)
# legal_name = address.get('address_title')
legal_name = gstin_details.get('LegalName')
trade_name = gstin_details.get('TradeName')
# location = address.get('city')
location = gstin_details.get('Loc')
state_code = address.get('gst_state_number')
pincode = cint(address.get('pincode'))
address_line1 = address.get('address_line1')
address_line2 = address.get('address_line2')
email_id = address.get('email_id')
phone = address.get('phone')
if state_code == 97:
pincode = 999999
return frappe._dict(dict(
gstin=gstin, legal_name=legal_name, location=location,
pincode=pincode, state_code=state_code, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone
))
def get_overseas_address_details(party_address):
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value(
"Address", party_address, ["address_title", "address_line1", "address_line2", "city", "phone", "email_id"]
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone,
pincode=999999, state_code=96, place_of_supply=96, location=city
))
def get_item_list(invoice):
item_list = []
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
for d in invoice.items:
item_schema = read_json("einv_item_template")
item = frappe._dict(dict())
item.update(d.as_dict())
item.sr_no = d.idx
item.description = d.item_name
item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y"
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.unit_rate = item.base_price_list_rate if item.discount_amount else item.base_rate
item.total_amount = item.unit_rate * item.qty
item.discount_amount = item.discount_amount * item.qty
item.tax_rate = 0
item.igst_amount = 0
item.cgst_amount = 0
item.sgst_amount = 0
item.cess_rate = 0
item.cess_amount = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts.cess_account:
item.cess_rate += item_tax_detail[0]
item.cess_amount += item_tax_detail[1]
elif t.account_head in gst_accounts.igst_account:
item.tax_rate += item_tax_detail[0]
item.igst_amount += item_tax_detail[1]
elif t.account_head in gst_accounts.sgst_account:
item.tax_rate += item_tax_detail[0]
item.sgst_amount += item_tax_detail[1]
elif t.account_head in gst_accounts.cgst_account:
item.tax_rate += item_tax_detail[0]
item.cgst_amount += item_tax_detail[1]
item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount
e_inv_item = item_schema.format(item=item)
item_list.append(e_inv_item)
return ", ".join(item_list)
def get_value_details(invoice):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
value_details = frappe._dict(dict())
value_details.base_net_total = invoice.base_net_total
value_details.invoice_discount_amt = invoice.discount_amount
value_details.round_off = invoice.base_rounding_adjustment
value_details.base_grand_total = invoice.base_rounded_total
value_details.grand_total = invoice.rounded_total
value_details.total_cgst_amt = 0
value_details.total_sgst_amt = 0
value_details.total_igst_amt = 0
value_details.total_cess_amt = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
value_details.total_cess_amt += t.base_tax_amount
elif t.account_head in gst_accounts.igst_account:
value_details.total_igst_amt += t.base_tax_amount
elif t.account_head in gst_accounts.sgst_account:
value_details.total_sgst_amt += t.base_tax_amount
elif t.account_head in gst_accounts.cgst_account:
value_details.total_cgst_amt += t.base_tax_amount
return value_details
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments])
paid_amount = invoice.base_paid_amount
outstanding_amount = invoice.outstanding_amount
return frappe._dict(dict(
payee_name=payee_name, mode_of_payment=mode_of_payment,
paid_amount=paid_amount, outstanding_amount=outstanding_amount
))
def get_return_doc_reference(invoice):
invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date")
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=invoice_date
))
def get_eway_bill_details(invoice):
if not invoice.distance:
frappe.throw(_("Distance is mandatory for E-Way Bill generation"), title=_("Missing Values"))
mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
return frappe._dict(dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
distance=invoice.distance,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
vehicle_no=invoice.vehicle_no,
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
))
@frappe.whitelist()
def make_e_invoice(doctype, name):
invoice = frappe.get_doc(doctype, name)
schema = read_json("einv_template")
validations = json.loads(read_json("einv_validation"))
trans_details = get_trans_details(invoice)
doc_details = get_doc_details(invoice)
seller_details = get_party_gstin_details(invoice.company_address)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_gstin_details(invoice.customer_address)
place_of_supply = invoice.place_of_supply.split('-')[0]
buyer_details.update(dict(place_of_supply=place_of_supply))
item_list = get_item_list(invoice)
value_details = get_value_details(invoice)
shipping_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
shipping_details = get_party_gstin_details(invoice.shipping_address_name)
payment_details = frappe._dict({})
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
prev_doc_details = frappe._dict({})
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
dispatch_details = frappe._dict({})
period_details = frappe._dict({})
export_details = frappe._dict({})
eway_bill_details = frappe._dict({})
if invoice.transporter:
eway_bill_details = get_eway_bill_details(invoice)
e_invoice = schema.format(
trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details,
seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
item_list=item_list, value_details=value_details, payment_details=payment_details,
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
e_invoice = json.loads(e_invoice)
error_msgs = validate_einvoice(validations, e_invoice, [])
if error_msgs:
if len(error_msgs) > 1:
li = ["<li>"+ d +"</li>" for d in error_msgs]
frappe.throw(_("""<ul style="padding-left: 20px">{}</ul>""").format("".join(li)), title=_("E Invoice Validation Failed"))
else:
frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed"))
return {'einvoice': json.dumps([e_invoice])}
def validate_einvoice(validations, e_invoice, error_msgs=[]):
type_map = {
"string": cstr,
"number": cint,
"object": dict,
"array": list
}
for field, value in validations.items():
if isinstance(value, list): value = value[0]
invoice_value = e_invoice.get(field)
if not invoice_value:
continue
should_be_of_type = type_map[value.get('type').lower()]
if should_be_of_type == dict:
properties = value.get('properties')
if isinstance(invoice_value, list):
for d in invoice_value:
validate_einvoice(properties, d, error_msgs)
else:
validate_einvoice(properties, invoice_value, error_msgs)
# remove keys with empty dicts
if not invoice_value:
e_invoice.pop(field, None)
continue
if invoice_value == "None":
# remove keys with empty values
e_invoice.pop(field, None)
continue
# convert to int or str
e_invoice[field] = should_be_of_type(invoice_value)
should_be_of_len = value.get('maxLength')
should_be_greater_than = flt(value.get('minimum'))
should_be_less_than = flt(value.get('maximum'))
pattern_str = value.get('pattern')
pattern = re.compile(pattern_str or "")
field_label = value.get("label") or field
if value.get('type').lower() == 'string' and len(invoice_value) > should_be_of_len:
error_msgs.append("{} should not exceed {} characters".format(field_label, should_be_of_len))
if value.get('type').lower() == 'number' and not (flt(invoice_value) <= should_be_less_than):
error_msgs.append("{} should be less than {}".format(field_label, should_be_less_than))
if pattern_str and not pattern.match(invoice_value):
error_msgs.append(value.get('validationMsg'))
return error_msgs
@frappe.whitelist()
def download_einvoice():
data = frappe._dict(frappe.local.form_dict)
einvoice = data['einvoice']
name = data['name']
frappe.response['filename'] = "E-Invoice-" + name + ".json"
frappe.response['filecontent'] = einvoice
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
@frappe.whitelist()
def upload_einvoice():
signed_einvoice = json.loads(frappe.local.uploaded_file)
data = frappe._dict(frappe.local.form_dict)
doctype = data['doctype']
name = data['docname']
enc_signed_invoice = signed_einvoice.get('SignedInvoice')
decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data']
frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn'))
frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo'))
frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode'))
frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice)
@frappe.whitelist()
def download_cancel_einvoice():
data = frappe._dict(frappe.local.form_dict)
name = data['name']
irn = data['irn']
reason = data['reason']
remark = data['remark']
cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)])
frappe.response['filename'] = "Cancel E-Invoice " + name + ".json"
frappe.response['filecontent'] = cancel_einvoice
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
@frappe.whitelist()
def upload_cancel_ack():
cancel_ack = json.loads(frappe.local.uploaded_file)
data = frappe._dict(frappe.local.form_dict)
doctype = data['doctype']
name = data['docname']
frappe.db.set_value(doctype, name, "irn_cancelled", 1)

View File

@@ -0,0 +1,26 @@
{{
"SlNo": "{item.sr_no}",
"PrdDesc": "{item.description}",
"IsServc": "{item.is_service_item}",
"HsnCd": "{item.gst_hsn_code}",
"Barcde": "{item.barcode}",
"Unit": "{item.uom}",
"Qty": "{item.qty}",
"FreeQty": "{item.free_qty}",
"UnitPrice": "{item.unit_rate}",
"TotAmt": "{item.total_amount}",
"Discount": "{item.discount_amount}",
"AssAmt": "{item.base_amount}",
"PrdSlNo": "{item.serial_no}",
"GstRt": "{item.tax_rate}",
"IgstAmt": "{item.igst_amount}",
"CgstAmt": "{item.cgst_amount}",
"SgstAmt": "{item.sgst_amount}",
"CesRt": "{item.cess_rate}",
"CesAmt": "{item.cess_amount}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
}}

View File

@@ -0,0 +1,109 @@
{{
"Version": "1.01",
"TranDtls": {{
"TaxSch": "{trans_details.tax_scheme}",
"SupTyp": "{trans_details.supply_type}",
"RegRev": "{trans_details.reverse_charge}",
"EcmGstin": "{trans_details.ecom_gstin}",
"IgstOnIntra": "{trans_details.igst_on_intra}"
}},
"DocDtls": {{
"Typ": "{doc_details.invoice_type}",
"No": "{doc_details.invoice_name}",
"Dt": "{doc_details.invoice_date}"
}},
"SellerDtls": {{
"Gstin": "{seller_details.gstin}",
"LglNm": "{seller_details.legal_name}",
"TrdNm": "{seller_details.trade_name}",
"Loc": "{seller_details.location}",
"Pin": "{seller_details.pincode}",
"Stcd": "{seller_details.state_code}",
"Addr1": "{seller_details.address_line1}",
"Addr2": "{seller_details.address_line2}",
"Ph": "{seller_details.phone}",
"Em": "{seller_details.email}"
}},
"BuyerDtls": {{
"Gstin": "{buyer_details.gstin}",
"LglNm": "{buyer_details.legal_name}",
"TrdNm": "{buyer_details.trade_name}",
"Addr1": "{buyer_details.address_line1}",
"Addr2": "{buyer_details.address_line2}",
"Loc": "{buyer_details.location}",
"Pin": "{buyer_details.pincode}",
"Stcd": "{buyer_details.state_code}",
"Ph": "{buyer_details.phone}",
"Em": "{buyer_details.email}",
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
"Nm": "{dispatch_details.company_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
"Pin": "{dispatch_details.pincode}",
"Stcd": "{dispatch_details.state_code}"
}},
"ShipDtls": {{
"Gstin": "{shipping_details.gstin}",
"LglNm": "{shipping_details.legal_name}",
"TrdNm": "{shipping_details.trader_name}",
"Addr1": "{shipping_details.address_line1}",
"Addr2": "{shipping_details.address_line2}",
"Loc": "{shipping_details.location}",
"Pin": "{shipping_details.pincode}",
"Stcd": "{shipping_details.state_code}"
}},
"ItemList": [
{item_list}
],
"ValDtls": {{
"AssVal": "{value_details.base_net_total}",
"CgstVal": "{value_details.total_cgst_amt}",
"SgstVal": "{value_details.total_sgst_amt}",
"IgstVal": "{value_details.total_igst_amt}",
"CesVal": "{value_details.total_cess_amt}",
"Discount": "{value_details.invoice_discount_amt}",
"RndOffAmt": "{value_details.round_off}",
"TotInvVal": "{value_details.base_grand_total}",
"TotInvValFc": "{value_details.grand_total}"
}},
"PayDtls": {{
"Nm": "{payment_details.payee_name}",
"AccDet": "{payment_details.account_no}",
"Mode": "{payment_details.mode_of_payment}",
"FinInsBr": "{payment_details.ifsc_code}",
"PayTerm": "{payment_details.terms}",
"PaidAmt": "{payment_details.paid_amount}",
"PaymtDue": "{payment_details.outstanding_amount}"
}},
"RefDtls": {{
"DocPerdDtls": {{
"InvStDt": "{period_details.start_date}",
"InvEndDt": "{period_details.end_date}"
}},
"PrecDocDtls": {{
"InvNo": "{prev_doc_details.invoice_name}",
"InvDt": "{prev_doc_details.invoice_date}"
}}
}},
"ExpDtls": {{
"ShipBNo": "{export_details.bill_no}",
"ShipBDt": "{export_details.bill_date}",
"Port": "{export_details.port}",
"ForCur": "{export_details.foreign_curr_code}",
"CntCode": "{export_details.country_code}",
"ExpDuty": "{export_details.export_duty}"
}},
"EwbDtls": {{
"TransId": "{eway_bill_details.gstin}",
"TransName": "{eway_bill_details.name}",
"TransMode": "{eway_bill_details.mode_of_transport}",
"Distance": "{eway_bill_details.distance}",
"TransDocNo": "{eway_bill_details.document_name}",
"TransDocDt": "{eway_bill_details.document_date}",
"VehNo": "{eway_bill_details.vehicle_no}",
"VehType": "{eway_bill_details.vehicle_type}"
}}
}}

View File

@@ -0,0 +1,836 @@
{
"Version": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"Irn": {
"type": "string",
"minLength": 64,
"maxLength": 64
},
"TranDtls": {
"type": "object",
"properties": {
"TaxSch": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["GST"]
},
"SupTyp": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"]
},
"RegRev": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"EcmGstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})"
},
"IgstOnIntra": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
}
},
"required": ["TaxSch", "SupTyp"]
},
"DocDtls": {
"type": "object",
"properties": {
"Typ": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"enum": ["INV", "CRN", "DBN"]
},
"No": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
"label": "Document Name",
"validationMsg": "Document name should not be starting with 0, / and -"
},
"Dt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Document Date is invalid"
}
},
"required": ["Typ", "No", "Dt"]
},
"SellerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"validationMsg": "Seller GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 50
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"BuyerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 3,
"maxLength": 15,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"validationMsg": "Buyer GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pos": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
},
"DispDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ShipDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"maxLength": 15,
"minLength": 3,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"validationMsg": "Shipping Address GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ItemList": [
{
"type": "object",
"properties": {
"SlNo": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"PrdDesc": {
"type": "string",
"minLength": 3,
"maxLength": 300
},
"IsServc": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"HsnCd": {
"type": "string",
"minLength": 4,
"maxLength": 8
},
"Barcde": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"Qty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"FreeQty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"Unit": {
"type": "string",
"minLength": 3,
"maxLength": 8
},
"UnitPrice": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.999
},
"TotAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"PreTaxVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"AssAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"GstRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"IgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"SgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"CesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"StateCesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"TotItemVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OrdLineRef": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"OrgCntry": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"PrdSlNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"BchDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 20
},
"ExpDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Expiry Date is invalid"
},
"WrDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Warranty Date is invalid"
}
},
"required": ["Nm"]
},
"AttribDtls": {
"type": "Array",
"Attribute": [
{
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"Val": {
"type": "string",
"minLength": 1,
"maxLength": 100
}
}
}
]
}
},
"required": [
"SlNo",
"IsServc",
"HsnCd",
"UnitPrice",
"TotAmt",
"AssAmt",
"GstRt",
"TotItemVal"
]
}
],
"ValDtls": {
"type": "object",
"properties": {
"AssVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CgstVal": {
"type": "number",
"maximum": 99999999999999.99,
"minimum": 0
},
"SgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"IgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"StCesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"RndOffAmt": {
"type": "number",
"minimum": 0,
"maximum": 99.99
},
"TotInvVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"TotInvValFc": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
},
"required": ["AssVal", "TotInvVal"]
},
"PayDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"AccDet": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"Mode": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"FinInsBr": {
"type": "string",
"minLength": 1,
"maxLength": 11
},
"PayTerm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"PayInstr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrTrn": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"DirDr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrDay": {
"type": "number",
"minimum": 0,
"maximum": 9999
},
"PaidAmt": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"PaymtDue": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
}
},
"RefDtls": {
"type": "object",
"properties": {
"InvRm": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"pattern": "^[0-9A-Za-z/-]{3,100}$"
},
"DocPerdDtls": {
"type": "object",
"properties": {
"InvStDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"InvEndDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
},
"required": ["InvStDt", "InvEndDt"]
},
"PrecDocDtls": {
"type": "object",
"properties": {
"InvNo": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$"
},
"InvDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"OthRefNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
}
},
"required": ["InvNo", "InvDt"]
},
"ContrDtls": {
"type": "object",
"properties": {
"RecAdvRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"RecAdvDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"TendRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ContrRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ExtRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ProjRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"PORefr": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([0-9A-Za-z/-]){1,16}$"
},
"PORefDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
}
}
},
"required": ["InvStDt", "InvEndDt"]
},
"AddlDocDtls": {
"type": "Array",
"AddlDocument": [
{
"type": "object",
"properties": {
"Url": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Docs": {
"type": "string",
"minLength": 3,
"maxLength": 1000
},
"Info": {
"type": "string",
"minLength": 3,
"maxLength": 1000
}
}
}
]
},
"ExpDtls": {
"type": "object",
"properties": {
"ShipBNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"ShipBDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"Port": {
"type": "string",
"minLength": 2,
"maxLength": 10,
"pattern": "^[0-9A-Za-z]{2,10}$"
},
"RefClm": {
"type": "string",
"minLength": 1,
"maxLength": 1
},
"ForCur": {
"type": "string",
"minLength": 3,
"maxLength": 16
},
"CntCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"ExpDuty": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
}
}
},
"EwbDtls": {
"type": "object",
"properties": {
"TransId": {
"type": "string",
"minLength": 15,
"maxLength": 15
},
"TransName": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TransMode": {
"type": "string",
"maxLength": 1,
"minLength": 1,
"enum": ["1", 2, 3, 4]
},
"Distance": {
"type": "number",
"minimum": 1,
"maximum": 9999
},
"TransDocNo": {
"type": "string",
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$"
},
"TransDocDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"VehNo": {
"type": "string",
"minLength": 4,
"maxLength": 20
},
"VehType": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["O", "R"]
}
},
"required": ["Distance"]
},
"required": [
"Version",
"TranDtls",
"DocDtls",
"SellerDtls",
"BuyerDtls",
"ItemList",
"ValDtls"
]
}

View File

@@ -0,0 +1,180 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
if (!einvoicing_enabled
|| !['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)) {
return;
}
// if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) {
// frm.add_custom_button(
// "Generate IRN",
// () => {
// frappe.call({
// method: 'erpnext.regional.india.e_invoice_utils.generate_irn',
// args: { doctype: frm.doc.doctype, name: frm.doc.name },
// freeze: true,
// callback: (res) => {
// console.log(res.message);
// frm.set_value('irn', res.message['Irn']);
// frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice']));
// frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode']));
// if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']);
// frm.save();
// }
// })
// }
// )
// }
// if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) {
// frm.add_custom_button(
// "Cancel IRN",
// () => {
// const d = new frappe.ui.Dialog({
// title: __('Cancel IRN'),
// fields: [
// { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate",
// "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] },
// { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 }
// ],
// primary_action: function() {
// const data = d.get_values();
// frappe.call({
// method: 'erpnext.regional.india.e_invoice_utils.cancel_irn',
// args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark },
// freeze: true,
// callback: () => {
// frm.set_value('irn_cancelled', 1);
// frm.save("Update");
// d.hide()
// },
// error: () => d.hide()
// })
// },
// primary_action_label: __('Submit')
// });
// d.show();
// }
// )
// }
// if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) {
// frm.add_custom_button(
// "Cancel E-Way Bill",
// () => {
// const d = new frappe.ui.Dialog({
// title: __('Cancel E-Way Bill'),
// fields: [
// { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate",
// "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] },
// { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 }
// ],
// primary_action: function() {
// const data = d.get_values();
// frappe.call({
// method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill',
// args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark },
// freeze: true,
// callback: () => {
// frm.set_value('eway_bill_cancelled', 1);
// frm.save("Update");
// d.hide()
// },
// error: () => d.hide()
// })
// },
// primary_action_label: __('Submit')
// });
// d.show();
// }
// )
// }
if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) {
frm.add_custom_button(
"Download E-Invoice",
() => {
frappe.call({
method: 'erpnext.regional.india.e_invoice_utils.make_e_invoice',
args: { doctype: frm.doc.doctype, name: frm.doc.name },
freeze: true,
callback: (res) => {
if (!res.exc) {
const args = {
cmd: 'erpnext.regional.india.e_invoice_utils.download_einvoice',
einvoice: res.message.einvoice,
name: frm.doc.name
};
open_url_post(frappe.request.url, args);
}
}
})
}, "E-Invoicing");
frm.add_custom_button(
"Upload Signed E-Invoice",
() => {
new frappe.ui.FileUploader({
method: 'erpnext.regional.india.e_invoice_utils.upload_einvoice',
allow_multiple: 0,
doctype: frm.doc.doctype,
docname: frm.doc.name,
on_success: (attachment, r) => {
if (!r.exc) {
frm.reload_doc();
}
}
});
}, "E-Invoicing");
}
if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) {
frm.add_custom_button(
"Cancel IRN",
() => {
const d = new frappe.ui.Dialog({
title: __('Cancel IRN'),
fields: [
{
"label" : "Reason", "fieldname": "reason",
"fieldtype": "Select", "reqd": 1, "default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1
}
],
primary_action: function() {
const data = d.get_values();
const args = {
cmd: 'erpnext.regional.india.e_invoice_utils.download_cancel_einvoice',
irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name
};
open_url_post(frappe.request.url, args);
d.hide();
},
primary_action_label: __('Download JSON')
});
d.show();
}, "E-Invoicing");
frm.add_custom_button(
"Upload Cancel JSON",
() => {
new frappe.ui.FileUploader({
method: 'erpnext.regional.india.e_invoice_utils.upload_cancel_ack',
allow_multiple: 0,
doctype: frm.doc.doctype,
docname: frm.doc.name,
on_success: (attachment, r) => {
if (!r.exc) {
frm.reload_doc();
}
}
});
}, "E-Invoicing");
}
}
})
}

View File

@@ -105,8 +105,17 @@ def add_print_formats():
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_property_setters():
# GST rules do not allow for an invoice no. bigger than 16 characters
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -118,6 +127,9 @@ def make_custom_fields(update=True):
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
print_hide=1)
taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
print_hide=1)
purchase_invoice_gst_category = [
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
@@ -178,15 +190,20 @@ def make_custom_fields(update=True):
purchase_invoice_itc_fields = [
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nIneligible\nAll Other ITC', default="All Other ITC"),
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
default="All Other ITC"),
dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax',
fieldtype='Data', insert_after='eligibility_for_itc', print_hide=1),
fieldtype='Currency', insert_after='eligibility_for_itc',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_central_tax', label='Availed ITC Central Tax',
fieldtype='Data', insert_after='itc_integrated_tax', print_hide=1),
fieldtype='Currency', insert_after='itc_integrated_tax',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax',
fieldtype='Data', insert_after='itc_central_tax', print_hide=1),
fieldtype='Currency', insert_after='itc_central_tax',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_cess_amount', label='Availed ITC Cess',
fieldtype='Data', insert_after='itc_state_tax', print_hide=1),
fieldtype='Currency', insert_after='itc_state_tax',
options='Company:company:default_currency', print_hide=1),
]
sales_invoice_gst_fields = [
@@ -216,6 +233,23 @@ def make_custom_fields(update=True):
depends_on="eval:doc.gst_category=='Overseas' "),
]
journal_entry_fields = [
dict(fieldname='reversal_type', label='Reversal Type',
fieldtype='Select', insert_after='voucher_type', print_hide=1,
options="As per rules 42 & 43 of CGST Rules\nOthers",
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_address', label='Company Address',
fieldtype='Link', options='Address', insert_after='reversal_type',
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
fetch_from='company_address.gstin',
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
]
inter_state_gst_field = [
dict(fieldname='is_inter_state', label='Is Inter State',
fieldtype='Check', insert_after='disabled', print_hide=1),
@@ -430,6 +464,7 @@ def make_custom_fields(update=True):
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
'Journal Entry': journal_entry_fields,
'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field,
'Item': [
@@ -444,10 +479,10 @@ def make_custom_fields(update=True):
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Salary Component': [
dict(fieldname= 'component_type',

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json
from frappe import _
import erpnext
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@@ -176,8 +176,6 @@ def get_regional_address_details(party_details, doctype, company):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
@@ -188,7 +186,6 @@ def get_regional_address_details(party_details, doctype, company):
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
@@ -220,6 +217,11 @@ def update_party_details(party_details, doctype):
if party_details.get(address_field):
party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
def update_party_details(party_details, doctype):
for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']:
if party_details.get(address_field):
party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
destination_gstin = party_details.company_gstin
@@ -255,20 +257,6 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
{'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
return default_tax
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))},
['gst_category', 'export_type'], as_dict=1)
if gst_details:
if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax':
default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0,
"gst_state": number_state_mapping[party_details.company_gstin[:2]]})
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
def calculate_annual_eligible_hra_exemption(doc):
basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
if not (basic_component and hra_component):
@@ -669,10 +657,19 @@ def validate_state_code(state_code, address):
return int(state_code)
@frappe.whitelist()
def get_gst_accounts(company, account_wise=False):
def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0):
filters={"parent": "GST Settings"}
if company:
filters.update({'company': company})
if only_reverse_charge:
filters.update({'is_reverse_charge_account': 1})
elif only_non_reverse_charge:
filters.update({'is_reverse_charge_account': 0})
gst_accounts = frappe._dict()
gst_settings_accounts = frappe.get_all("GST Account",
filters={"parent": "GST Settings", "company": company},
filters=filters,
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
if not gst_settings_accounts and not frappe.flags.in_test:
@@ -687,101 +684,63 @@ def get_gst_accounts(company, account_wise=False):
return gst_accounts
def update_grand_total_for_rcm(doc, method):
def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return
base_gst_tax = 0
base_reverse_charge_booked = 0
if doc.reverse_charge == 'Y':
doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax
doc.base_total_taxes_and_charges -= base_gst_tax
gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1)
reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
update_totals(gst_tax, base_gst_tax, doc)
def update_totals(gst_tax, base_gst_tax, doc):
doc.base_grand_total -= base_gst_tax
doc.grand_total -= gst_tax
if doc.meta.get_field("rounded_total"):
if doc.is_rounded_total_disabled():
doc.outstanding_amount = doc.grand_total
else:
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
doc.currency, doc.precision("rounded_total"))
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
doc.precision("rounding_adjustment"))
doc.outstanding_amount = doc.rounded_total or doc.grand_total
doc.in_words = money_in_words(doc.grand_total, doc.currency)
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
doc.set_payment_schedule()
def make_regional_gl_entries(gl_entries, doc):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return gl_entries
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return gl_entries
if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if tax.account_head in non_reverse_charge_accounts:
if tax.add_deduct_tax == 'Add':
base_gst_tax += tax.base_tax_amount_after_discount_amount
else:
base_gst_tax += tax.base_tax_amount_after_discount_amount
elif tax.account_head in reverse_charge_accounts:
if tax.add_deduct_tax == 'Add':
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
else:
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
account_currency = get_account_currency(tax.account_head)
if base_gst_tax != base_reverse_charge_booked:
msg = _("Booked reverse charge is not equal to applied tax amount")
msg += "<br>"
msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format(
gst_document_link='<a href="https://docs.erpnext.com/docs/user/manual/en/regional/india/gst-setup">GST Documentation</a>')
gl_entries.append(doc.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"posting_date": doc.posting_date,
"against": doc.supplier,
dr_or_cr: tax.base_tax_amount_after_discount_amount,
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
if account_currency==doc.company_currency \
else tax.tax_amount_after_discount_amount
}, account_currency, item=tax)
)
frappe.throw(msg)
return gl_entries
def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
def get_gst_tax_amount(doc):
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
+ gst_accounts.get('igst_account', [])
if country != 'India':
return
base_gst_tax = 0
gst_tax = 0
# Initialize values
doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0
gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
return gst_tax, base_gst_tax
if tax.account_head in gst_accounts.get('igst_account'):
doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('sgst_account'):
doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('cgst_account'):
doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('cess_account'):
doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount)
@frappe.whitelist()
def get_regional_round_off_accounts(company, account_list):
@@ -803,3 +762,48 @@ def get_regional_round_off_accounts(company, account_list):
account_list.extend(gst_account_list)
return account_list
def update_taxable_values(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
gst_accounts = get_gst_accounts(doc.company)
# Only considering sgst account to avoid inflating taxable value
gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \
+ gst_accounts.get('igst_account', [])
additional_taxes = 0
total_charges = 0
item_count = 0
considered_rows = []
for tax in doc.get('taxes'):
prev_row_id = cint(tax.row_id) - 1
if tax.account_head in gst_account_list and prev_row_id not in considered_rows:
if tax.charge_type == 'On Previous Row Amount':
additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount
considered_rows.append(prev_row_id)
if tax.charge_type == 'On Previous Row Total':
additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total
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
applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)),
item.precision('taxable_value')))
item.taxable_value = applicable_charges + proportionate_value
total_charges += applicable_charges
item_count += 1
if total_charges != additional_taxes:
diff = additional_taxes - total_charges
doc.get('items')[item_count - 1].taxable_value += diff

View File

@@ -46,7 +46,13 @@ frappe.query_reports["GSTR-1"] = {
"label": __("Type of Business"),
"fieldtype": "Select",
"reqd": 1,
"options": ["B2B", "B2C Large", "B2C Small", "CDNR", "EXPORT"],
"options": [
{ "value": "B2B", "label": __("B2B Invoices - 4A, 4B, 4C, 6B, 6C") },
{ "value": "B2C Large", "label": __("B2C(Large) Invoices - 5A, 5B") },
{ "value": "B2C Small", "label": __("B2C(Small) Invoices - 7") },
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") }
],
"default": "B2B"
}
],

View File

@@ -32,6 +32,7 @@ class Gstr1Report(object):
reverse_charge,
return_against,
is_return,
is_debit_note,
gst_category,
export_type,
port_code,
@@ -42,7 +43,7 @@ class Gstr1Report(object):
def run(self):
self.get_columns()
self.gst_accounts = get_gst_accounts(self.filters.company)
self.gst_accounts = get_gst_accounts(self.filters.company, only_non_reverse_charge=1)
self.get_invoice_data()
if self.invoices:
@@ -62,9 +63,9 @@ class Gstr1Report(object):
for rate, items in items_based_on_rate.items():
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
if self.filters.get("type_of_business") == "CDNR":
if self.filters.get("type_of_business") == "CDNR-REG":
row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N")
row.append("C" if invoice_details.return_against else "R")
row.append("C" if invoice_details.is_return else "D")
if taxable_value:
self.data.append(row)
@@ -105,7 +106,7 @@ class Gstr1Report(object):
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
row = []
for fieldname in self.invoice_fields:
if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value":
if self.filters.get("type_of_business") == "CDNR-REG" and fieldname == "invoice_value":
row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total))
elif fieldname == "invoice_value":
row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total)
@@ -171,7 +172,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B":
conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1"
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -179,19 +180,19 @@ class Gstr1Report(object):
frappe.throw(_("Please set B2C Limit in GST Settings."))
if self.filters.get("type_of_business") == "B2C Large":
conditions += """ and ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ and (
conditions += """ AND (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "CDNR":
conditions += """ and is_return = 1 """
elif self.filters.get("type_of_business") == "CDNR-REG":
conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')"""
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ and is_return !=1 and gst_category = 'Overseas' """
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
return conditions
def get_invoice_items(self):
@@ -199,7 +200,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict()
items = frappe.db.sql("""
select item_code, parent, base_net_amount, item_tax_rate
select item_code, parent, taxable_value, base_net_amount, item_tax_rate
from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -207,7 +208,7 @@ class Gstr1Report(object):
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum(i.get('base_net_amount', 0) for i in items
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items
if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {}
@@ -403,7 +404,7 @@ class Gstr1Report(object):
"width": 100
}
]
elif self.filters.get("type_of_business") == "CDNR":
elif self.filters.get("type_of_business") == "CDNR-REG":
self.invoice_columns = [
{
"fieldname": "customer_gstin",
@@ -437,6 +438,17 @@ class Gstr1Report(object):
"options": "Sales Invoice",
"width":120
},
{
"fieldname": "reverse_charge",
"label": "Reverse Charge",
"fieldtype": "Data"
},
{
"fieldname": "export_type",
"label": "Export Type",
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "reason_for_issuing_document",
"label": "Reason For Issuing document",
@@ -449,6 +461,11 @@ class Gstr1Report(object):
"fieldtype": "Data",
"width": 120
},
{
"fieldname": "gst_category",
"label": "GST Category",
"fieldtype": "Data"
},
{
"fieldname": "invoice_value",
"label": "Invoice Value",
@@ -458,10 +475,10 @@ class Gstr1Report(object):
]
self.other_columns = [
{
"fieldname": "cess_amount",
"label": "Cess Amount",
"fieldtype": "Currency",
"width": 100
"fieldname": "cess_amount",
"label": "Cess Amount",
"fieldtype": "Currency",
"width": 100
},
{
"fieldname": "pre_gst",
@@ -589,6 +606,12 @@ def get_json(filters, report_name, data):
out = get_export_json(res)
gst_json["exp"] = out
elif filters["type_of_business"] == 'CDNR-REG':
for item in report_data[:-1]:
res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
out = get_cdnr_reg_json(res, gstin)
gst_json["cdnr"] = out
return {
'report_name': report_name,
@@ -628,7 +651,6 @@ def get_b2b_json(res, gstin):
return out
def get_b2cs_json(data, gstin):
company_state_number = gstin[0:2]
out = []
@@ -713,6 +735,54 @@ def get_export_json(res):
return out
def get_cdnr_reg_json(res, gstin):
out = []
for gst_in in res:
cdnr_item, inv = {"ctin": gst_in, "nt": []}, []
if not gst_in: continue
for number, invoice in iteritems(res[gst_in]):
if not invoice[0]["place_of_supply"]:
frappe.throw(_("""{0} not entered in Invoice {1}.
Please update and try again""").format(frappe.bold("Place Of Supply"),
frappe.bold(invoice[0]['invoice_number'])))
inv_item = {
"nt_num": invoice[0]["invoice_number"],
"nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'),
"val": abs(flt(invoice[0]["invoice_value"])),
"ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"],
"inv_type": get_invoice_type_for_cdnr(invoice[0])
}
inv_item["itms"] = []
for item in invoice:
inv_item["itms"].append(get_rate_and_tax_details(item, gstin))
inv.append(inv_item)
if not inv: continue
cdnr_item["nt"] = inv
out.append(cdnr_item)
return out
def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY':
invoice_type = 'SEWP'
else:
invoice_type = 'SEWOP'
elif row.get('gst_category') == 'Deemed Export':
row.invoice_type = 'DE'
elif row.get('gst_category') == 'Registered Regular':
invoice_type = 'R'
return invoice_type
def get_basic_invoice_detail(row):
return {
"inum": row["invoice_number"],

View File

@@ -16,6 +16,8 @@
"customer_name",
"gender",
"customer_type",
"pan",
"tax_withholding_category",
"default_bank_account",
"lead_name",
"image",
@@ -479,13 +481,25 @@
"fieldname": "dn_required",
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
},
{
"fieldname": "pan",
"fieldtype": "Data",
"label": "PAN"
},
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
}
],
"icon": "fa fa-user",
"idx": 363,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-03-17 11:03:42.706907",
"modified": "2021-01-28 12:54:57.258959",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -18,6 +18,8 @@
"dn_required",
"sales_update_frequency",
"maintain_same_sales_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"editable_price_list_rate",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
@@ -133,6 +135,23 @@
"fieldname": "hide_tax_id",
"fieldtype": "Check",
"label": "Hide Customer's Tax ID from Sales Transactions"
},
{
"default": "Stop",
"depends_on": "maintain_same_sales_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"mandatory_depends_on": "maintain_same_sales_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval: doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
}
],
"icon": "fa fa-cog",
@@ -140,7 +159,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:12:56.784014",
"modified": "2021-04-04 20:18:12.814624",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -23,7 +23,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if search_value:
data = search_serial_or_batch_or_barcode_number(search_value)
item_code = data.get("item_code") if data.get("item_code") else search_value
serial_no = data.get("serial_no") if data.get("serial_no") else ""
batch_no = data.get("batch_no") if data.get("batch_no") else ""
@@ -31,7 +31,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if data:
item_info = frappe.db.get_value(
"Item", data.get("item_code"),
"Item", data.get("item_code"),
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"]
, as_dict=1)
item_info.setdefault('serial_no', serial_no)
@@ -257,4 +257,4 @@ def set_customer_info(fieldname, customer, value=""):
elif fieldname == 'mobile_no':
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
frappe.db.set_value('Customer', customer, 'mobile_no', value)
contact_doc.save()
contact_doc.save()

View File

@@ -45,7 +45,7 @@ erpnext.PointOfSale.Controller = class {
{
fieldname: "opening_amount", fieldtype: "Currency",
in_list_view: 1, label: "Opening Amount",
options: "company:company_currency",
options: "company:company_currency",
change: function () {
dialog.fields_dict.balance_details.df.data.some(d => {
if (d.idx == this.doc.idx) {
@@ -207,7 +207,7 @@ erpnext.PointOfSale.Controller = class {
if (this.frm.doc.items.length == 0) {
frappe.show_alert({
message:__("You must add atleast one item to save it as draft."),
message:__("You must add atleast one item to save it as draft."),
indicator:'red'
});
frappe.utils.play_sound("error");
@@ -216,7 +216,7 @@ erpnext.PointOfSale.Controller = class {
this.frm.save(undefined, undefined, undefined, () => {
frappe.show_alert({
message:__("There was an error saving the document."),
message:__("There was an error saving the document."),
indicator:'red'
});
frappe.utils.play_sound("error");
@@ -439,7 +439,7 @@ erpnext.PointOfSale.Controller = class {
})
}
toggle_recent_order_list(show) {
this.toggle_components(!show);
@@ -549,7 +549,7 @@ erpnext.PointOfSale.Controller = class {
const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
this.update_cart_html(item_row);
@@ -587,7 +587,7 @@ erpnext.PointOfSale.Controller = class {
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
}
}
} catch (error) {
console.log(error);
} finally {
@@ -598,7 +598,7 @@ erpnext.PointOfSale.Controller = class {
get_item_from_frm(item_code, batch_no, uom) {
const has_batch_no = batch_no;
return this.frm.doc.items.find(
i => i.item_code === item_code
i => i.item_code === item_code
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
&& (i.uom === uom)
);
@@ -627,7 +627,7 @@ erpnext.PointOfSale.Controller = class {
const no_serial_selected = !item_row.serial_no;
const no_batch_selected = !item_row.batch_no;
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
return true;
}

View File

@@ -259,6 +259,7 @@ erpnext.company.setup_queries = function(frm) {
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],
["exchange_gain_loss_account", {"root_type": "Expense"}],
@@ -275,7 +276,7 @@ erpnext.company.setup_queries = function(frm) {
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
["unrealized_profit_loss_account", {"root_type": "Liability"}]
["unrealized_profit_loss_account", {"root_type": "Liability"},]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});

View File

@@ -59,6 +59,7 @@
"default_deferred_expense_account",
"default_payroll_payable_account",
"default_expense_claim_payable_account",
"default_discount_account",
"section_break_22",
"cost_center",
"column_break_26",
@@ -733,6 +734,12 @@
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
},
{
"fieldname": "default_discount_account",
"fieldtype": "Link",
"label": "Default Payment Discount Account",
"options": "Account"
}
],
"icon": "fa fa-building",
@@ -740,7 +747,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2020-12-03 12:27:27.085094",
"modified": "2021-02-16 11:12:03.400217",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -73,6 +73,34 @@ frappe.ui.form.on("Purchase Receipt", {
})
}, __('Create'));
}
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.docstatus == 0) {
frm.add_custom_button(__('Purchase Invoice'), function () {
if (!frm.doc.supplier) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Supplier")
});
}
erpnext.utils.map_current_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
source_doctype: "Purchase Invoice",
target: frm,
setters: {
supplier: frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
per_received: ["<", 100],
company: frm.doc.company
}
})
}, __("Get Items From"));
}
},
company: function(frm) {

View File

@@ -53,7 +53,20 @@ class PurchaseReceipt(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty',
'percent_join_field': 'material_request'
},
{
'source_dt': 'Purchase Receipt Item',
'target_dt': 'Purchase Invoice Item',
'join_field': 'purchase_invoice_item',
'target_field': 'received_qty',
'target_parent_dt': 'Purchase Invoice',
'target_parent_field': 'per_received',
'target_ref_field': 'qty',
'source_field': 'received_qty',
'percent_join_field': 'purchase_invoice',
'overflow_type': 'receipt'
}]
if cint(self.is_return):
self.status_updater.extend([
{
@@ -513,7 +526,9 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
for d in self.get("items"):
if d.purchase_order_item:
if d.purchase_invoice and d.purchase_invoice_item:
d.db_set('billed_amt', d.amount, update_modified=update_modified)
elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified)
for pr in set(updated_pr):

View File

@@ -66,16 +66,18 @@
"warehouse",
"rejected_warehouse",
"from_warehouse",
"purchase_order",
"material_request",
"purchase_order",
"purchase_invoice",
"column_break_40",
"is_fixed_asset",
"asset_location",
"asset_category",
"schedule_date",
"quality_inspection",
"purchase_order_item",
"material_request_item",
"purchase_order_item",
"purchase_invoice_item",
"purchase_receipt_item",
"delivery_note_item",
"putaway_rule",
@@ -890,12 +892,69 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "discount_and_margin_section",
"fieldtype": "Section Break",
"label": "Discount and Margin"
},
{
"depends_on": "price_list_rate",
"fieldname": "margin_type",
"fieldtype": "Select",
"label": "Margin Type",
"options": "\nPercentage\nAmount",
"print_hide": 1
},
{
"depends_on": "eval:doc.margin_type && doc.price_list_rate",
"fieldname": "margin_rate_or_amount",
"fieldtype": "Float",
"label": "Margin Rate or Amount",
"print_hide": 1
},
{
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
"fieldname": "rate_with_margin",
"fieldtype": "Currency",
"label": "Rate With Margin",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_37",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
"fieldname": "base_rate_with_margin",
"fieldtype": "Currency",
"label": "Rate With Margin (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"options": "Purchase Invoice",
"read_only": 1
},
{
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-01-30 21:44:06.918515",
"modified": "2021-03-29 04:17:00.336298",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -13,6 +13,7 @@
"column_break_4",
"valuation_method",
"over_delivery_receipt_allowance",
"role_allowed_to_over_deliver_receive",
"action_if_quality_inspection_is_not_submitted",
"show_barcode_field",
"clean_description_html",
@@ -234,6 +235,13 @@
"fieldname": "disable_serial_no_and_batch_selector",
"fieldtype": "Check",
"label": "Disable Serial No And Batch Selector"
},
{
"description": "Users with this role are allowed to over deliver/receive against orders above the allowance perecentage",
"fieldname": "role_allowed_to_over_deliver_receive",
"fieldtype": "Link",
"label": "Role Allowed to Over Deliver/Receive",
"options": "Role"
}
],
"icon": "icon-cog",
@@ -241,7 +249,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-18 13:15:38.352796",
"modified": "2021-03-11 18:48:14.513055",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -13,37 +13,36 @@ def get_level():
min_count = 0
doctypes = {
"Asset": 5,
"BOM": 3,
"Customer": 5,
"BOM": 3,
"Customer": 5,
"Delivery Note": 5,
"Employee": 3,
"Instructor": 5,
"Instructor": 5,
"Employee": 3,
"Instructor": 5,
"Instructor": 5,
"Issue": 5,
"Item": 5,
"Journal Entry": 3,
"Item": 5,
"Journal Entry": 3,
"Lead": 3,
"Leave Application": 5,
"Material Request": 5,
"Opportunity": 5,
"Payment Entry": 2,
"Opportunity": 5,
"Payment Entry": 2,
"Project": 5,
"Purchase Order": 2,
"Purchase Order": 2,
"Purchase Invoice": 5,
"Purchase Receipt": 5,
"Quotation": 3,
"Salary Slip": 5,
"Salary Structure": 5,
"Sales Order": 2,
"Sales Invoice": 2,
"Sales Order": 2,
"Sales Invoice": 2,
"Stock Entry": 3,
"Student": 5,
"Student": 5,
"Supplier": 5,
"Task": 5,
"User": 5,
"User": 5,
"Work Order": 5
}
for doctype, min_count in iteritems(doctypes):
count = frappe.db.count(doctype)
if count > min_count:

View File

@@ -120,11 +120,11 @@ class TransactionBase(StatusUpdater):
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
if self.doctype in buying_doctypes:
to_disable = "Maintain same rate throughout Purchase cycle"
settings_page = "Buying Settings"
action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
settings_doc = "Buying Settings"
else:
to_disable = "Maintain same rate throughout Sales cycle"
settings_page = "Selling Settings"
action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
settings_doc = "Selling Settings"
for ref_dt, ref_dn_field, ref_link_field in ref_details:
for d in self.get("items"):
@@ -132,11 +132,16 @@ class TransactionBase(StatusUpdater):
ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01:
frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ")
.format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.")
.format(frappe.bold(_(to_disable)),
get_link_to_form(settings_page, settings_page, frappe.bold(settings_page))))
if action == "Stop":
role_allowed_to_override = frappe.db.get_single_value(settings_doc, 'role_to_override_stop_action')
if role_allowed_to_override not in frappe.get_roles():
frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
else:
frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate), title=_("Warning"), indicator="orange")
def get_link_filters(self, for_doctype):
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):