Compare commits

...

294 Commits

Author SHA1 Message Date
Maharshi Patel
a16347f325 fix: add Document Date in E-Invoice print format 2022-11-08 16:35:34 +05:30
Deepesh Garg
19b9875ba8 Merge pull request #32781 from frappe/mergify/bp/version-13-hotfix/pr-32777
fix: Reset advance paid amount on Order cancel and amend (backport #32777)
2022-11-08 10:53:02 +05:30
Deepesh Garg
43aa670e90 Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into mergify/bp/version-13-hotfix/pr-32777 2022-11-08 10:40:07 +05:30
Deepesh Garg
4f76ed1c68 chore: Resolve conflicts 2022-11-08 10:39:21 +05:30
Deepesh Garg
b93d13feef Merge pull request #32865 from frappe/mergify/bp/version-13-hotfix/pr-32846
fix: add german translations (backport #32846)
2022-11-07 18:42:38 +05:30
Raffael Meyer
e1a32cc620 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32846 2022-11-07 13:18:06 +01:00
barredterra
550f5f280c chore: resolve merge conflicts 2022-11-07 13:15:38 +01:00
Deepesh Garg
fbbfeb8563 Merge pull request #32873 from frappe/mergify/bp/version-13-hotfix/pr-32802
fix: `Material Consumption` option in case of `Skip Transfer to WIP` in WO (backport #32802)
2022-11-07 10:18:14 +05:30
Sagar Sharma
418c131331 fix: Material Consumption option in case of Skip Transfer to WIP in WO
(cherry picked from commit 8ea6983734)
2022-11-07 04:06:55 +00:00
Raffael Meyer
a8bf17e560 chore: add german translations (#32846)
Mostly for balance sheet

(cherry picked from commit d2b6490bca)

# Conflicts:
#	erpnext/translations/de.csv
2022-11-06 04:53:43 +00:00
Deepesh Garg
9961037f71 Merge pull request #32860 from frappe/mergify/bp/version-13-hotfix/pr-32794
fix: Disable tax included prices for internal transfers (backport #32794)
2022-11-05 21:17:09 +05:30
Deepesh Garg
84ee1b86af fix: Disable tax included prices for internal transfers (#32794)
* fix: Disable tax-included prices for internal transfers

(cherry picked from commit 8d30ebb12b)
2022-11-05 15:22:30 +00:00
Deepesh Garg
afe86d83aa Merge pull request #32857 from frappe/mergify/bp/version-13-hotfix/pr-32847
fix: Create POS Opening Entry POS Profile filter. (backport #32847)
2022-11-05 20:52:12 +05:30
Maharshi Patel
76e4bb44f1 fix: Create POS Opening Entry POS Profile filter.
pos_profile_query was variable instead of function.

(cherry picked from commit 1328a45f2a)
2022-11-05 11:15:58 +00:00
Deepesh Garg
c1bc1040b8 Merge pull request #32834 from frappe/mergify/bp/version-13-hotfix/pr-32773
fix: for asset's purchase_date, if bill_date is set, use that instead of posting_date (backport #32773)
2022-11-03 12:06:50 +05:30
anandbaburajan
1d23c9a9fd fix: for asset's purchase_date, if bill_date is set, use that instead of posting_date
(cherry picked from commit f322c608cf)
2022-11-03 06:28:37 +00:00
Sagar Sharma
5a211813d3 Merge pull request #32821 from frappe/mergify/bp/version-13-hotfix/pr-32788
fix: use `flt` instead of `cint` in `get_batch_no` (backport #32788)
2022-11-02 17:08:08 +05:30
Sagar Sharma
601b1e3821 fix: use flt instead of cint in get_batch_no
(cherry picked from commit 9fb3fb4c83)
2022-11-02 10:25:54 +00:00
Deepesh Garg
a91483899b Merge pull request #32806 from frappe/mergify/bp/version-13-hotfix/pr-32779
fix: Mode of payment for returns in POS Sales Invoice (backport #32779)
2022-11-01 22:11:50 +05:30
Deepesh Garg
2f6d55d8d2 Merge pull request #32803 from frappe/mergify/bp/version-13-hotfix/pr-32801
fix: Issues while cancel/amending Purchase Invoice with TDS enabled (backport #32801)
2022-11-01 22:11:26 +05:30
Deepesh Garg
4a10454d89 chore: Resolve conflicts 2022-11-01 21:30:36 +05:30
Deepesh Garg
12b15347bc chore: Update tests
(cherry picked from commit 5b74161195)
2022-11-01 15:59:54 +00:00
Deepesh Garg
9b63a1a2e9 fix: Mode of payment for returns in POS Sales Invoice
(cherry picked from commit 06e8e28531)
2022-11-01 15:59:53 +00:00
Deepesh Garg
8888957952 fix: Issues while cancel/amending Purchase Invoice with TDS enabled
(cherry picked from commit f7c9258770)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
2022-11-01 15:18:48 +00:00
ruthra kumar
a9660bf026 Merge pull request #32798 from ruthra-kumar/better_approach_to_list_item_removal
fix: filter return pos in reconciliation tool
2022-11-01 17:28:01 +05:30
ruthra kumar
b877b0d3a2 fix: filter return pos in reconciliation tool 2022-11-01 16:56:35 +05:30
Deepesh Garg
e4082093b8 Merge pull request #32796 from deepeshgarg007/itc_eligibility_reverse_charge
fix: Do not force eligibilgity of itc for reverse charge
2022-11-01 16:32:39 +05:30
Deepesh Garg
9dc0edfb8a fix: Do not force eligibilgity of itc for reverse charge 2022-11-01 16:03:32 +05:30
rohitwaghchaure
e13f05ce19 Merge pull request #32784 from rohitwaghchaure/fixed-warehouse-group-for-batch
fix: group warehouse filter not working for Batch-wise Balance history report
2022-10-31 23:34:00 +05:30
Rohit Waghchaure
310e7c522c fix: group warehouse filter not working for Batch-wise Balance history report 2022-10-31 23:21:29 +05:30
Deepesh Garg
e32e0bc8fa fix: Reset advance paid amount on Oreder cancel and amend
(cherry picked from commit 92f37ca111)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.js
2022-10-31 16:09:48 +00:00
Deepesh Garg
0c24ba0118 Merge pull request #32690 from frappe/mergify/bp/version-13-hotfix/pr-32424
feat: Repayment schedule types for term loans (backport #32424)
2022-10-31 11:57:57 +05:30
Deepesh Garg
60fa87751a chore: Update payroll loan tests 2022-10-31 11:28:32 +05:30
Deepesh Garg
50bc7785f7 chore: reload doctypes 2022-10-31 10:39:03 +05:30
Deepesh Garg
d2aea0cd2e chore: fix datetime value 2022-10-30 11:01:41 +05:30
Deepesh Garg
e9006582fb chore: Add removed field 2022-10-30 10:48:37 +05:30
Deepesh Garg
3483e0e2a5 Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into mergify/bp/version-13-hotfix/pr-32424 2022-10-30 10:46:21 +05:30
Deepesh Garg
65b047c94c chore: Resync Loan Doc 2022-10-30 10:44:15 +05:30
Sagar Sharma
ada3ce21c0 Merge pull request #32757 from frappe/mergify/bp/version-13-hotfix/pr-32754
fix: add `Sales Order` reference in Material Request Dashboard (backport #32754)
2022-10-29 13:52:28 +05:30
Sagar Sharma
cb89dba5ab fix: add Sales Order reference in Material Request Dashboard
(cherry picked from commit 15ebf4a0cf)
2022-10-29 07:07:45 +00:00
Deepesh Garg
d73237e896 chore: Resolve conflicts 2022-10-29 11:06:53 +05:30
Sagar Sharma
8e0d393cd4 Merge pull request #32741 from frappe/mergify/bp/version-13-hotfix/pr-32738
fix: Added Material Request Reference in Purchase Recipt Dashboard for Tracking (backport #32738)
2022-10-28 16:06:35 +05:30
Vishal
292ff1bd6b chore: minor linting issue fixed
(cherry picked from commit e8c0157017)
2022-10-28 08:55:07 +00:00
Vishal
804cf40f69 chore: Added Material Request Reference in Purchase Recipt Dashboard for Tracking
(cherry picked from commit a04c44fe34)
2022-10-28 08:55:06 +00:00
Deepesh Garg
85f48dd6bd chore: Update tests
(cherry picked from commit e59b147a62)
2022-10-23 14:09:20 +00:00
Deepesh Garg
fa0a137a38 chore: Add repayment date on option
(cherry picked from commit ef0cb17faf)
2022-10-23 14:09:19 +00:00
Deepesh Garg
5fc8d20e96 chore: label post save
(cherry picked from commit bf7a51791a)
2022-10-23 14:09:18 +00:00
Deepesh Garg
7ddcfc00fc chore: Add patch to update repayment schedule type in loan documents
(cherry picked from commit 679b5ed551)

# Conflicts:
#	erpnext/patches.txt
2022-10-23 14:09:17 +00:00
Deepesh Garg
3f02059085 chore: Update labels as per repayment type
(cherry picked from commit 2ddee50f27)

# Conflicts:
#	erpnext/loan_management/doctype/loan/loan.json
2022-10-23 14:09:14 +00:00
Deepesh Garg
71b21086b1 chore: Remove print statements
(cherry picked from commit 3466461eb3)
2022-10-23 14:09:13 +00:00
Deepesh Garg
c2817bed0b feat: Repayment schedule types for term loans
(cherry picked from commit 76c6ccab5d)

# Conflicts:
#	erpnext/loan_management/doctype/loan/loan.json
#	erpnext/loan_management/doctype/loan/loan.py
2022-10-23 14:09:12 +00:00
Deepesh Garg
dc972718d5 Merge pull request #32687 from frappe/mergify/bp/version-13-hotfix/pr-32650
fix: unset contact details (backport #32650)
2022-10-23 18:32:15 +05:30
barredterra
7afb19f965 fix: unset contact details
(cherry picked from commit 23f0bb45b0)
2022-10-23 12:52:29 +00:00
Deepesh Garg
0ca137808b Merge pull request #32656 from frappe/mergify/bp/version-13-hotfix/pr-32641
fix: allow to create Sales Order from expired Quotation (backport #32641)
2022-10-20 17:34:31 +05:30
rohitwaghchaure
99e0106193 Merge pull request #32670 from frappe/mergify/bp/version-13-hotfix/pr-32667
fix: BOM cost update message (backport #32667)
2022-10-20 16:52:56 +05:30
rohitwaghchaure
447485ae90 fix: conflicts 2022-10-20 15:01:22 +05:30
Rohit Waghchaure
98bcb7255d fix: BOM cost update message
(cherry picked from commit 9cfe527492)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/test_bom.py
2022-10-20 09:09:02 +00:00
Deepesh Garg
2df24ec459 Merge pull request #32664 from frappe/mergify/bp/version-13-hotfix/pr-32659
fix: Billing Address for inter-company purchase docs (backport #32659)
2022-10-20 12:42:38 +05:30
Deepesh Garg
3a2f08fbda fix: Billing Address for inter-company purchase docs
(cherry picked from commit 796f2d3c09)
2022-10-20 06:29:56 +00:00
rohitwaghchaure
7c9b535514 Merge pull request #32661 from frappe/mergify/bp/version-13-hotfix/pr-32654
fix: incorrect qty in material request created from PP (backport #32654)
2022-10-20 10:27:41 +05:30
Rohit Waghchaure
4f0c2909ab test: validate qty and purchase uom in material request which is created from PP
(cherry picked from commit 4d5ef721f7)
2022-10-20 04:30:29 +00:00
Rohit Waghchaure
1741501ea8 fix: incorrect qty in material request
(cherry picked from commit ad278b2007)
2022-10-20 04:30:28 +00:00
Raffael Meyer
ea032893d3 fix: allow to create Sales Order from expired Quotation (#32641)
(cherry picked from commit 4ad3002861)
2022-10-19 16:38:19 +00:00
rohitwaghchaure
7601a016b8 Merge pull request #32653 from frappe/mergify/bp/version-13-hotfix/pr-32645
fix: overlap error not raised for job card in case of workstation with production capacity (backport #32645)
2022-10-19 18:09:44 +05:30
Rohit Waghchaure
b5376ce5cb fix: overlap error not raised for job card in case of workstation with production capacity
(cherry picked from commit 8b2165e0d1)
2022-10-19 11:35:59 +00:00
mergify[bot]
b8d97b82b8 fix: pricing rule item code UOM apply & conversions (backport #32566) (#32636)
* fix: pricing rule for non stock UOM and conversions

* fix: pricing rule for non stock UOM and conversions

(cherry picked from commit 96b4211ea1)

# Conflicts:
#	erpnext/accounts/doctype/pricing_rule/pricing_rule.py

* chore: resolve conflicts

Co-authored-by: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2022-10-18 23:31:31 +05:30
Deepesh Garg
56c4c3186b Merge pull request #32624 from frappe/mergify/bp/version-13-hotfix/pr-32622
fix: Ignore linked purchase invoice on cancel (backport #32622)
2022-10-18 14:49:36 +05:30
Deepesh Garg
015a221d8d chore: Resolve conflicts 2022-10-18 09:11:58 +05:30
Deepesh Garg
eec770a4a8 fix: Ignore linked purchase invoice on cancel
(cherry picked from commit faadf78332)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
2022-10-17 14:09:12 +00:00
Deepesh Garg
44f5ccc9dc Merge pull request #32605 from frappe/mergify/bp/version-13-hotfix/pr-32594
fix: Party account for multi-order invoices (backport #32594)
2022-10-15 18:25:25 +05:30
Deepesh Garg
0ae2a4f1c4 fix: Party account for multi-order invoices
(cherry picked from commit fd49503ba2)
2022-10-15 11:29:21 +00:00
mergify[bot]
5fdaddad86 chore: drop dead code (backport #32595) (#32597)
chore: drop dead code (#32595)

[skip ci]

(cherry picked from commit 50e9698932)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-13 15:27:09 +05:30
Sagar Sharma
03ccddc1cc Merge pull request #32587 from frappe/mergify/bp/version-13-hotfix/pr-32576
fix: `Brand Defaults` filters (backport #32576)
2022-10-13 14:42:29 +05:30
Sagar Sharma
e00ebcd9e5 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32576 2022-10-13 14:38:26 +05:30
mergify[bot]
7e7122b668 fix: don't try to update youtube data if disabled in settings (backport #32588) (#32590)
* fix: don't try to update youtube data if disabled in settings (#32588)

fix: cast value from db

[skip ci]

(cherry picked from commit e543dca6a0)

* chore: qualified path

not imported 

[skip ci]

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-13 11:57:08 +05:30
Sagar Sharma
b1c70af4db fix: Brand Defaults filters
(cherry picked from commit 7da32c7db3)
2022-10-12 13:53:56 +00:00
Deepesh Garg
ff3bfb060a Merge pull request #32584 from frappe/mergify/bp/version-13-hotfix/pr-32557
fix: Value error on validation of POS invoices with Serial Nos (backport #32557)
2022-10-12 17:17:01 +05:30
rohitwaghchaure
63e087d31e Merge pull request #32580 from frappe/mergify/bp/version-13-hotfix/pr-32563
fix: consider sales rate as incoming rate for transit warehouse in purchase flow (backport #32563)
2022-10-12 17:08:33 +05:30
Deepesh Garg
b62a397397 Merge pull request #32581 from frappe/mergify/bp/version-13-hotfix/pr-32272
fix: Incoming rate precision fixes for intra company transfer (backport #32272)
2022-10-12 17:04:52 +05:30
ruthra kumar
48efcc82b6 test: value error on serial no validation on pos
(cherry picked from commit 9e2bd10d03)
2022-10-12 11:12:09 +00:00
ruthra kumar
4383980bcc fix: value error on pos submit
(cherry picked from commit 4b908ebcd6)
2022-10-12 11:12:08 +00:00
rohitwaghchaure
aa0552d788 fix: linter issue 2022-10-12 16:33:53 +05:30
Deepesh Garg
3b5889ef85 chore: resolve conflicts 2022-10-12 16:30:23 +05:30
rohitwaghchaure
8238b89907 fix: incorrect import 2022-10-12 16:28:31 +05:30
rohitwaghchaure
6ae2f90683 fix: conflct in test purchase receipt 2022-10-12 16:26:52 +05:30
rohitwaghchaure
cb7aef505d fix: conflict 2022-10-12 16:24:50 +05:30
Deepesh Garg
4a8c42d62b chore: check only for inter-company transfers
(cherry picked from commit 9aa5e20ef7)
2022-10-12 10:54:15 +00:00
Deepesh Garg
c67bdcf3c6 chore: fix precision condition
(cherry picked from commit 49601558c6)
2022-10-12 10:54:14 +00:00
Deepesh Garg
7a9e7b66ac chore: Use proper accounts
(cherry picked from commit 1c05c004cd)
2022-10-12 10:54:13 +00:00
Deepesh Garg
151b9d4d7b chore: Increase precision for other doc fields
(cherry picked from commit c8d2181498)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
2022-10-12 10:54:13 +00:00
Deepesh Garg
b966f10711 chore: GL Entries for SLE diff
(cherry picked from commit df2a0e265b)
2022-10-12 10:54:11 +00:00
Deepesh Garg
0b48f13873 test: Internal tranfer precision loss test
(cherry picked from commit dc20b21fb5)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2022-10-12 10:54:10 +00:00
Deepesh Garg
b531a38efe chore: Increase incoming_rate field precision to 6
(cherry picked from commit b31c3bd35d)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
2022-10-12 10:54:08 +00:00
Deepesh Garg
dd26ef96e0 fix: Hanlde rounding loss for internal transfer
(cherry picked from commit 6e47fd54a0)
2022-10-12 10:54:07 +00:00
Deepesh Garg
227ce5f8a2 fix: Incoming rate precision fixes for intra company transfer
(cherry picked from commit 083309c056)
2022-10-12 10:54:06 +00:00
Rohit Waghchaure
acd64ba7c1 fix: test case
(cherry picked from commit 98bf8e1304)
2022-10-12 10:51:22 +00:00
Rohit Waghchaure
6f43133c04 fix: consider outgoingrate while valuation rate calculate
(cherry picked from commit 3266e54e33)
2022-10-12 10:51:22 +00:00
Rohit Waghchaure
f72602ebf3 fix: consider sales rate as incoming rate for transit warehouse in purchase flow
(cherry picked from commit 683a47f7a1)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
#	erpnext/stock/stock_ledger.py
2022-10-12 10:51:21 +00:00
Ankush Menat
002ae8ae13 ci: disable test orchestrator v13 (#32574) 2022-10-12 12:13:51 +05:30
Deepesh Garg
558dc57b94 Merge pull request #32539 from frappe/mergify/bp/version-13-hotfix/pr-32536
fix: PO cancel post advance payment cancel against PO (backport #32536)
2022-10-11 13:58:24 +05:30
Deepesh Garg
cd942d36a8 chore: resolve conflicts 2022-10-11 10:16:31 +05:30
Sagar Sharma
3257533ee3 Merge pull request #32544 from frappe/mergify/bp/version-13-hotfix/pr-32497
chore: set `Quality Inspection` status based on readings status (backport #32497)
2022-10-10 12:10:29 +05:30
Sagar Sharma
2763d06307 fix(test): test_rejected_qi_validation
(cherry picked from commit 4992e4a2b8)
2022-10-10 05:21:09 +00:00
Sagar Sharma
a0772ea8d7 test: add test cases for Quality Inspection status
(cherry picked from commit fcc1272d42)
2022-10-10 05:21:08 +00:00
Sagar Sharma
d67b44fcec fix: set Quality Inspection status based on readings status
(cherry picked from commit 2657ece2cd)
2022-10-10 05:21:08 +00:00
Sagar Sharma
257a2a3d71 fix: make readings status mandatory in Quality Inspection
(cherry picked from commit d7c3b7633a)
2022-10-10 05:21:07 +00:00
Sagar Sharma
e6abbd1c83 chore: add Manual Inspection field in Quality Inspection DocType
(cherry picked from commit 39707757a6)
2022-10-10 05:21:06 +00:00
Deepesh Garg
41599cf29f fix: PO cancel post advance payment cancel against PO
(cherry picked from commit d806e32030)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py
2022-10-09 13:05:46 +00:00
Deepesh Garg
997990c69f Merge pull request #32523 from frappe/mergify/bp/version-13-hotfix/pr-32522
fix: Tax withholding related fixes (backport #32522)
2022-10-07 18:08:31 +05:30
Deepesh Garg
2bf76f64bd chore: resolve conflicts 2022-10-07 17:33:03 +05:30
Deepesh Garg
e1c41b9195 fix: Do not add tax withheld vouchers post tax withheding in one document
(cherry picked from commit 781d160c68)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2022-10-07 11:03:16 +00:00
Deepesh Garg
32a9575f07 fix: Tax withholding related fixes
(cherry picked from commit abf5b6be3e)
2022-10-07 11:03:15 +00:00
Deepesh Garg
e031e095dd Merge pull request #32521 from FHenry/13_fr_translation
chore: fr translation (backport #32385)
2022-10-07 08:48:43 +05:30
Florian HENRY
39ce3756c8 chore: fr translation 2022-10-06 22:39:40 +02:00
Deepesh Garg
f2515d9c9c Merge pull request #32449 from rtdany10/cst-ux-improvement
feat(ux): improved course scheduling tool
2022-10-05 18:44:05 +05:30
Deepesh Garg
27fe9c9179 Merge pull request #32426 from AnandBaburajan/fix_mark_attendance_joining_date_and_future
fix: mark attendance issue with joining and relieving date, and fix future attendance marking
2022-10-05 18:34:31 +05:30
Deepesh Garg
503183cb0b Merge pull request #32502 from frappe/mergify/bp/version-13-hotfix/pr-32499
fix: TooManyWritesError during reposting of stock (backport #32499)
2022-10-05 17:29:25 +05:30
Deepesh Garg
f1ea5de022 Merge pull request #32504 from frappe/mergify/bp/version-13-hotfix/pr-32478
feat(JE): trigger account field when fetched from template (backport #32478)
2022-10-05 17:29:05 +05:30
Dany Robert
baa4fec611 feat(JE): trigger account field when fetched from template
Closes #32409

(cherry picked from commit c35adcf5a1)
2022-10-05 10:38:21 +00:00
Rohit Waghchaure
476175b307 fix: TooManyWritesError during reposting of stock
(cherry picked from commit aaabba9b1e)
2022-10-05 10:35:55 +00:00
rohitwaghchaure
3648745e5e Merge pull request #32470 from frappe/mergify/bp/version-13-hotfix/pr-32466
fix: not able to return sold expired batches (backport #32466)
2022-10-04 07:35:10 +05:30
Deepesh Garg
8e1e6a194b Merge pull request #32467 from frappe/mergify/bp/version-13-hotfix/pr-32394
fix: fetch swift number in payment request from bank doctype (backport #32394)
2022-10-03 21:54:05 +05:30
Ankush Menat
663e9b403f chore: codeowners 2022-10-03 16:27:32 +05:30
Ankush Menat
3219b7c766 chore: codeowners 2022-10-03 16:27:13 +05:30
Sagar Sharma
3a6e4c8da7 Merge pull request #32474 from frappe/mergify/bp/version-13-hotfix/pr-32472
fix: pick list picked-qty for batch item (backport #32472)
2022-10-03 15:24:02 +05:30
Sagar Sharma
96a6db0422 fix: pick list picked-qty for batch item
(cherry picked from commit ba02209f1d)
2022-10-03 09:17:03 +00:00
rohitwaghchaure
4757a65863 fix: linter issue 2022-10-03 13:27:38 +05:30
rohitwaghchaure
0f8e34e972 fix: conflict 2022-10-03 13:18:58 +05:30
Rohit Waghchaure
2fd4485f2f fix: not able to return sold expired batches
(cherry picked from commit 0b1727cf79)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2022-10-03 07:46:13 +00:00
Maharshi Patel
299175e6fe fix: payment request make bank field Link instead of Read Only
(cherry picked from commit dc8d49260c)
2022-10-03 07:26:06 +00:00
Maharshi Patel
8251b843ec Revert "fix: fetch swift_number in payment_request"
This reverts commit f42a8e4e03.

(cherry picked from commit 9245d3b5cd)
2022-10-03 07:26:05 +00:00
Maharshi Patel
60745705a8 fix: fetch swift_number in payment_request
There isn't direct link between payment_request and bank so swift_number wasn't fetched using Fetch form. I fixed it by fetching swift_number on_change of bank_account.

(cherry picked from commit f42a8e4e03)
2022-10-03 07:26:04 +00:00
Deepesh Garg
caedc9f6ad Merge pull request #32462 from frappe/mergify/bp/version-13-hotfix/pr-32456
fix: update with new Frappe color. fix #32455 (backport #32456)
2022-10-03 09:15:21 +05:30
Deepesh Garg
fbeb86b9c0 Merge pull request #32453 from deepeshgarg007/intra_state_transfer_eway_bill_v13
fix: e-Way bill JSON for Intra-state internal transfers
2022-10-03 09:07:26 +05:30
Muvuk
ad43b18ace fix: update with new Frappe color. fix #32455 (#32456)
* Update with new Frappe color.

* refactor: use css variables

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
(cherry picked from commit 73e5a7d671)
2022-10-02 17:02:25 +00:00
Deepesh Garg
9f8d3e43ea Merge pull request #32413 from AnandBaburajan/fix_asset_sold_status_v13
fix: status of assets with maintenance_required changing back to 'Partially Depreciated' some time after being sold [v13]
2022-10-02 17:43:09 +05:30
Deepesh Garg
9dbb2bf512 Merge pull request #32369 from maharshivpatel/fix-taxes-sez-without-payment-of-tax
fix: SEZ Without Payment of Tax don't add tax rows
2022-10-01 16:13:11 +05:30
Deepesh Garg
8c52c71299 Merge pull request #32344 from maharshivpatel/fix-payment-reconciliation-jv-cost-center
fix: consider cost center for journal entry in payment reconciliation tool
2022-10-01 16:12:30 +05:30
Deepesh Garg
c4c52c1cd3 chore: Linting Issues 2022-10-01 16:06:31 +05:30
Deepesh Garg
b7fbf75e10 fix: e-Way bill JSON for Intra-state internal transfers 2022-10-01 15:58:31 +05:30
Dany Robert
6dabfbedf1 Merge branch 'version-13-hotfix' into cst-ux-improvement 2022-10-01 14:19:31 +05:30
Dany Robert
68c592377b chore: pre-commit 2022-09-30 23:41:01 -07:00
Deepesh Garg
d69d2217f6 Merge pull request #32431 from frappe/mergify/bp/version-13-hotfix/pr-32402
fix: Item details fetching on making transaction from item dashboard (backport #32402)
2022-10-01 12:05:26 +05:30
mergify[bot]
cdc8297083 fix: typo in sales_register's filter mode_of_payment (backport #32371) (#32447)
fix: typo in sales_register's filter mode_of_payment (#32371)
2022-10-01 12:04:40 +05:30
Dany Robert
f24f71f387 Merge branch 'version-13-hotfix' into cst-ux-improvement 2022-10-01 12:03:05 +05:30
Dany Robert
552c5951bd feat: cst ux improvement 2022-09-30 23:25:55 -07:00
Deepesh Garg
562025eb45 Merge branch 'version-13-hotfix' into fix-payment-reconciliation-jv-cost-center 2022-10-01 11:10:21 +05:30
Deepesh Garg
f0a37f545a chore: resolve conflicts 2022-10-01 11:02:40 +05:30
Deepesh Garg
001afb96ba Merge pull request #32435 from frappe/mergify/bp/version-13-hotfix/pr-32412
fix: Incorrect TCS amount deducted in Sales Invoice (backport #32412)
2022-10-01 11:01:05 +05:30
Deepesh Garg
61d3370527 chore: Remove print statements
(cherry picked from commit bff3cd9068)
2022-09-30 10:36:20 +00:00
Deepesh Garg
32456bff30 fix: Incorrect TCS amount deducted in Sales Invoice
(cherry picked from commit 08443c6421)
2022-09-30 10:36:20 +00:00
Deepesh Garg
1aed76f778 fix: Item details fetching on making transaction from item dashboard
(cherry picked from commit 0439e41a44)

# Conflicts:
#	erpnext/stock/doctype/item/item.js
2022-09-30 10:27:36 +00:00
anandbaburajan
308c400c6a chore: make linter happy 2022-09-30 15:33:34 +05:30
anandbaburajan
a9546dd01f fix: mark attendance issue with relieving date 2022-09-30 15:27:03 +05:30
anandbaburajan
cb4fbd5432 fix: future attendance marking 2022-09-30 14:48:30 +05:30
anandbaburajan
6f8d62088e fix: mark attendance issue with joining date 2022-09-30 14:46:57 +05:30
Anand Baburajan
5d66646fcd Merge branch 'version-13-hotfix' into fix_asset_sold_status_v13 2022-09-29 22:49:10 +05:30
Sagar Sharma
6a58d15497 Merge pull request #32419 from frappe/mergify/bp/version-13-hotfix/pr-32404
fix(ux): show `Make Purchase Invoice` button based on permission (backport #32404)
2022-09-29 17:16:15 +05:30
anandbaburajan
6b21deedae chore: refactor by just using a filter 2022-09-29 15:37:04 +05:30
Sagar Sharma
7124328640 fix: show Make Purchase Invoice button based on permission
(cherry picked from commit 80080a3d7b)
2022-09-29 09:27:03 +00:00
anandbaburajan
430a4c98a0 chore: refactor by creating is_sold 2022-09-29 08:59:49 +05:30
Anand Baburajan
c65fb64578 Merge branch 'version-13-hotfix' into fix_asset_sold_status_v13 2022-09-28 21:36:48 +05:30
anandbaburajan
14ab9d9158 fix: asset requiring maintenance sold status 2022-09-28 21:25:42 +05:30
Deepesh Garg
1095052479 Merge pull request #32410 from frappe/mergify/bp/version-13-hotfix/pr-32403
fix: Disbursement Account in patch to update old loans (backport #32403)
2022-09-28 20:13:45 +05:30
Deepesh Garg
b4a511cbb4 fix: Disbursement Account in patch to update old loans
(cherry picked from commit be623ce8e8)
2022-09-28 14:06:42 +00:00
Deepesh Garg
0b2405bbdf Merge pull request #32395 from frappe/mergify/bp/version-13-hotfix/pr-32379
fix: POS only validate QTY if is_stock_item (backport #32379)
2022-09-28 15:12:47 +05:30
Rucha Mahabal
255aa7a84a fix: don't count half day in absent days in Monthly Attendance Sheet summarized view (#32399) 2022-09-28 15:02:21 +05:30
Deepesh Garg
a604eed1b9 chore: Resolve conflicts 2022-09-28 14:49:25 +05:30
Maharshi Patel
96fa14be88 fix: POS properly validate stock for bundle products
Stock availability was not calculated properly for Product Bundle with non stock item so i have added logic to properly calculate that as well.

(cherry picked from commit e392ea1104)

# Conflicts:
#	erpnext/selling/page/point_of_sale/pos_item_details.js
2022-09-28 08:16:02 +00:00
Maharshi Patel
ac8100f1e5 fix: POS only validate QTY if is_stock_item
POS invoice raised " Item not available " validation error even though item is non_stock.

(cherry picked from commit e39e088f18)
2022-09-28 08:16:00 +00:00
Deepesh Garg
07cc05785e Merge pull request #32386 from frappe/mergify/bp/version-13-hotfix/pr-32382
fix: Move subscription process to hourly long queue (backport #32382)
2022-09-28 10:39:08 +05:30
Deepesh Garg
f03fbc0e6d chore: Resolve conflicts 2022-09-28 08:10:37 +05:30
Deepesh Garg
447c553954 fix: Move subscription process to hourly long quque
(cherry picked from commit 82a2f31ada)

# Conflicts:
#	erpnext/hooks.py
2022-09-27 18:09:32 +00:00
Deepesh Garg
b2e9dccc8c Merge pull request #32384 from frappe/mergify/bp/version-13-hotfix/pr-32378
fix: Add return against indexes for POS Invoice (backport #32378)
2022-09-27 23:34:50 +05:30
Deepesh Garg
9e8e7aab70 fix: Add return against indexes for POS Invoice
(cherry picked from commit cbfe28286a)
2022-09-27 16:52:59 +00:00
Deepesh Garg
6c528d469d fix: Add return against indexes for POS Invoice
(cherry picked from commit 1f6205e1ea)
2022-09-27 16:52:59 +00:00
Maharshi Patel
988c5b95e6 fix: GST Itemised Sales Register GSTIN filter (#32367)
fix: GST Itemised Sales Register GSTIN filte
2022-09-27 22:21:14 +05:30
Sagar Sharma
1f2887d601 Merge pull request #32381 from frappe/mergify/bp/version-13-hotfix/pr-32377
fix: consider overproduction percentage for WO finish button (backport #32377)
2022-09-27 18:51:37 +05:30
Sagar Sharma
ed4ac100ba fix: consider overproduction percentage for WO finish button
(cherry picked from commit 05392e0918)
2022-09-27 12:15:43 +00:00
Sagar Sharma
a194d28b69 fix: For Quantity error msg in Stock Entry
(cherry picked from commit 9049db41ae)
2022-09-27 12:15:42 +00:00
rohitwaghchaure
3217924242 Merge pull request #32373 from frappe/mergify/bp/version-13-hotfix/pr-32370
fix: Not allowing to return expired batches using purchase return (backport #32370)
2022-09-27 14:44:30 +05:30
Rohit Waghchaure
02468a902f fix: allow to return expired batches using purchase return
(cherry picked from commit a4a86ee23f)
2022-09-27 08:51:26 +00:00
Maharshi Patel
a13eecc961 fix: SEZ Without Payment of Tax don't add tax rows
taxes were added even when gst_category was SEZ and export_type was Without Payment of Tax
2022-09-27 13:40:04 +05:30
rohitwaghchaure
776ee53a25 Merge pull request #32358 from frappe/mergify/bp/version-13-hotfix/pr-32346
refactor: rewrite `Item Price Stock Report` queries in `QB` (backport #32346)
2022-09-27 10:44:05 +05:30
rohitwaghchaure
ddf5565c67 Merge pull request #32366 from frappe/mergify/bp/version-13-hotfix/pr-32339
fix: opening entry causing discrepancy between stock and trial balance (backport #32339)
2022-09-27 09:57:01 +05:30
Maharshi Patel
4152a9f026 Merge branch 'version-13-hotfix' into fix-payment-reconciliation-jv-cost-center 2022-09-26 23:03:39 +05:30
Saqib Ansari
8f961abe8b fix(e-invoicing): local variable 'res' referenced before assignment (#32352) 2022-09-26 22:42:07 +05:30
Rohit Waghchaure
70c68f011a fix: opening entry causing discepancy between stock and trial balance
(cherry picked from commit bc3ab45af2)
2022-09-26 15:03:48 +00:00
Sagar Sharma
9066009e89 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32346 2022-09-26 18:18:41 +05:30
Sagar Sharma
9332e7f96f Merge pull request #32360 from frappe/mergify/bp/version-13-hotfix/pr-32347
refactor: rewrite `Incorrect Stock Value Report` queries in `QB` (backport #32347)
2022-09-26 18:18:01 +05:30
Sagar Sharma
d6a335e59f refactor: rewrite Incorrect Stock Value Report queries in QB
(cherry picked from commit b93331e844)
2022-09-26 11:40:21 +00:00
Sagar Sharma
a66002f774 refactor: rewrite Item Price Stock Report queries in QB
(cherry picked from commit 22299d2382)
2022-09-26 11:39:24 +00:00
ruthra kumar
6eb3428f8e Merge pull request #32348 from frappe/mergify/bp/version-13-hotfix/pr-32303
fix: difference amount calculation and popup on payment reconciliation (backport #32303)
2022-09-26 14:18:09 +05:30
ruthra kumar
d158784472 Merge pull request #32350 from frappe/mergify/bp/version-13-hotfix/pr-32310
fix: total value in all keys (backport #32310)
2022-09-26 12:05:04 +05:30
nishibakabeer
80d046a38c fix: total value in all keys
Gross and net profit report showing wrong values in monthly quarterly and half yearly filters which is the total value
@ruthra-kumar added in develop branch as suggested ( https://github.com/frappe/erpnext/pull/32020)

(cherry picked from commit 6919f389aa)
2022-09-26 06:03:15 +00:00
ruthra kumar
d68cdb4f5e fix: difference amount calculation on payment reconciliation
(cherry picked from commit 122d5f2729)
2022-09-26 10:36:08 +05:30
Maharshi Patel
9f988910b5 fix: payment reconciliation tool consider cost_center for JV
We need to consider cost center in all four cases of get_conditions. I have removed check in if statement for get_invoices, get_payments, get_return_invoices as it is not required.
2022-09-25 23:24:59 +05:30
Deepesh Garg
09cd480c81 Merge pull request #32333 from frappe/mergify/bp/version-13-hotfix/pr-32117
fix: Reduce font size for Process Statement of accounts print/pdf (backport #32117)
2022-09-25 17:20:57 +05:30
Sagar Sharma
4777991a74 Merge pull request #32338 from frappe/mergify/bp/version-13-hotfix/pr-32324
refactor: rewrite `Production Planning Report` queries in `QB` (backport #32324)
2022-09-23 10:38:20 +05:30
Sagar Sharma
1301a6ff7f refactor: rewrite Production Planning Report queries in QB
(cherry picked from commit 8417b9b99c)
2022-09-23 04:20:42 +00:00
Sagar Sharma
a6a5f63af2 Merge pull request #32328 from s-aga-r/backport/v13-h/32309
fix: item_code key error in production plan (backport #32309)
2022-09-23 09:48:25 +05:30
Deepesh Garg
1cfeb9371c fix: Reduce font size for Process Statement of accounts print/pdf
(cherry picked from commit 6bfd193b0d)
2022-09-22 19:38:55 +00:00
Rohit Waghchaure
638d5e9dc3 fix: item_code key error in production plan 2022-09-22 18:09:12 +05:30
Sagar Sharma
b4ee72e15e Merge pull request #32316 from s-aga-r/backport/v13-h/32304
refactor: rewrite `Exponential Smoothing Forecasting` queries in `QB` (backport #32304)
2022-09-22 11:16:22 +05:30
Sagar Sharma
2a9b519773 Merge branch 'version-13-hotfix' into backport/v13-h/32304 2022-09-22 01:32:20 +05:30
Sagar Sharma
57136fa921 Merge pull request #32315 from s-aga-r/backport/v13-h/32153
refactor: rewrite Work Order Stock Report queries in QB (backport #32153)
2022-09-22 01:31:18 +05:30
Sagar Sharma
882542c2d5 Merge branch 'version-13-hotfix' into backport/v13-h/32153 2022-09-22 01:31:04 +05:30
Sagar Sharma
0a4025e7e0 Merge pull request #32314 from s-aga-r/backport/v13-h/32161
refactor: rewrite Process Loss Report queries in QB (backport #32161)
2022-09-22 01:30:40 +05:30
Sagar Sharma
54dfd50391 refactor: rewrite Exponential Smoothing Forecasting queries in QB 2022-09-21 21:27:04 +05:30
Sagar Sharma
45d02ceb4e refactor: rewrite Work Order Stock Report queries in QB 2022-09-21 21:20:03 +05:30
Sagar Sharma
f72bb18da7 refactor: rewrite Process Loss Report queries in QB 2022-09-21 21:13:10 +05:30
Deepesh Garg
f371008cd9 Merge pull request #32293 from frappe/mergify/bp/version-13-hotfix/pr-32284
fix: remove no_copy for ignore_pricing_rule (backport #32284)
2022-09-21 16:41:25 +05:30
Deepesh Garg
82e5a784cb Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32284 2022-09-21 13:08:41 +05:30
Deepesh Garg
d267372334 Merge pull request #32299 from frappe/mergify/bp/version-13-hotfix/pr-32296
fix: get amount in words for debit note (backport #32296)
2022-09-21 13:07:39 +05:30
Sagar Sharma
92c7d074dd Merge pull request #32307 from s-aga-r/backport/13/refactor/report/exponential-smoothing-forecasting
refactor: rewrite `BOM Variance Report` queries in `QB` (backport #32297)
2022-09-21 12:42:10 +05:30
Sagar Sharma
cedc8d28e4 refactor: rewrite BOM Variance Report queries in QB 2022-09-21 12:13:05 +05:30
Deepesh Garg
d0bd78ddcd Merge pull request #32289 from frappe/mergify/bp/version-13-hotfix/pr-32204
fix(UX): More predictable tax withholding application in invoices (backport #32204)
2022-09-21 00:11:37 +05:30
Deepesh Garg
e2912caeae chore: Handle edge cases 2022-09-20 23:50:16 +05:30
Sagar Sharma
6f9c2e6c80 Merge pull request #32302 from frappe/mergify/bp/version-13-hotfix/pr-32295
refactor: rewrite `BOM Stock Report` queries in `QB` (backport #32295)
2022-09-20 23:10:19 +05:30
Sagar Sharma
96bf1e2a0a fix: warehouse filter in BOM Stock Calculated Report
(cherry picked from commit 390ce5719d)
2022-09-20 17:37:41 +00:00
Sagar Sharma
1f633b293d refactor: rewrite BOM Stock Report queries in QB
(cherry picked from commit 8fd7c04920)
2022-09-20 17:37:40 +00:00
ruthra kumar
0cd1cacc29 fix: get amount in words for debit note
(cherry picked from commit 70f6484d9d)
2022-09-20 12:43:45 +00:00
Deepesh Garg
21154c8bee chore: fix tests 2022-09-20 17:52:37 +05:30
ruthra kumar
9270e58969 Merge pull request #32277 from frappe/remove_return_pos_from_reconciliation_tool
fix: remove return pos from pos reconciliation tool
2022-09-20 17:20:25 +05:30
Deepesh Garg
de8f44bbff chore: Resolve conflicts 2022-09-20 16:42:30 +05:30
ruthra kumar
e9bf74e589 fix: remove return pos from pos reconciliation tool 2022-09-20 16:41:28 +05:30
Maharshi Patel
8abfdb6598 fix: remove no_copy for ignore_pricing_rule
(cherry picked from commit 8c5b420aea)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
2022-09-20 10:49:59 +00:00
Nabin Hait
305693b562 Merge pull request #32281 from maharshivpatel/fix-item-wise-sales-register-v13
fix: item wise sales register taxes and charges
2022-09-20 14:56:23 +05:30
Deepesh Garg
794eeebabc chore: resolve conflicts 2022-09-20 14:48:06 +05:30
Deepesh Garg
75768d780f Merge pull request #32282 from frappe/mergify/bp/version-13-hotfix/pr-32217
fix: incorrect gl if tax on multi currency payment entry (backport #32217)
2022-09-20 14:39:37 +05:30
Deepesh Garg
fe252b48f7 chore: fix tests
(cherry picked from commit 9aa1f84d45)
2022-09-20 09:07:06 +00:00
Deepesh Garg
ca0cce7599 fix: TDS deduction via journal entry
(cherry picked from commit 36d0906ea2)
2022-09-20 09:07:05 +00:00
Deepesh Garg
e07fd46a46 test: Add tests
(cherry picked from commit b6184ce471)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2022-09-20 09:07:04 +00:00
Deepesh Garg
51d7c0bfe4 fix: Fetch vouchers to show in Invoice
(cherry picked from commit 3fb1595a4e)
2022-09-20 09:07:03 +00:00
Deepesh Garg
4165fee6a7 fix: Add child table for tax withheld vouchers
(cherry picked from commit 246c1a9380)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2022-09-20 09:07:02 +00:00
Deepesh Garg
d251ef9ce5 Merge pull request #32279 from frappe/mergify/bp/version-13-hotfix/pr-32235
fix: fetch description only if empty on the payment schedule (backport #32235)
2022-09-20 14:35:35 +05:30
Sagar Sharma
afda4bdb17 Merge pull request #32286 from frappe/mergify/bp/version-13-hotfix/pr-32280
refactor: rewrite `Item Shortage Report` queries in QB (backport #32280)
2022-09-20 11:43:41 +05:30
Sagar Sharma
c4452360da test: add test cases for Item Shortage Report
(cherry picked from commit 3dc754cac2)
2022-09-20 05:45:29 +00:00
Sagar Sharma
13527d6154 refactor: rewrite Item Shortage Report queries in QB
(cherry picked from commit f0a78aa559)
2022-09-20 05:45:28 +00:00
ruthra kumar
5c0a46110e test: gl entries of payments with advance tax
(cherry picked from commit 5bd5dd7262)
2022-09-20 11:09:37 +05:30
ruthra kumar
d9d6a07bbb fix: incorrect gl if tax on multi currency payment entry
(cherry picked from commit f0ae77b23b)
2022-09-20 04:30:42 +00:00
Maharshi Patel
e65990eb34 fix: item wise sales register taxes and charges
I have added a separate column for other charges. Instead of adding all values to tax_total, it checks if account_type is tax, and then only it adds to total_tax otherwise it adds to the total_other_charges.
2022-09-20 09:47:47 +05:30
Maharshi Patel
07a7fdbe6c fix: fetch description only if empty on the payment schedule
added fetch_if_empty on description field of payment_schedule.

(cherry picked from commit f4b64686ae)
2022-09-20 03:44:50 +00:00
Sagar Sharma
2fca8b541e Merge pull request #32261 from frappe/mergify/bp/version-13-hotfix/pr-32250
fix: make `po_detail` or `sco_rm_detail` mandatory for SE Send to Subcontractor (backport #32250)
2022-09-19 19:53:57 +05:30
Sagar Sharma
7fc460bb32 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32250 2022-09-19 19:22:31 +05:30
Solufyin
e2e69dced7 fix: Change Department fieldtype to Link in Leave Balance Report (#32252)
Co-authored-by: Nihantra C. Patel <n.patel.serpentcs@gmail.com>
2022-09-19 15:42:05 +05:30
Deepesh Garg
5e49b6ea0f Merge pull request #32269 from frappe/mergify/bp/version-13-hotfix/pr-32251
fix: use default supplier currency if default supplier is enabled (backport #32251)
2022-09-19 15:20:44 +05:30
Ritwik Puri
2e3445fad9 chore: patch for removing stale values in Naming Series (#32271) 2022-09-19 13:35:08 +05:30
ruthra kumar
3c10e5066a fix: use default supplier currency if default supplier is enabled
(cherry picked from commit 77fdc37cb7)
2022-09-19 07:32:58 +00:00
Sagar Sharma
e8d2e49155 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32250 2022-09-18 20:30:21 +05:30
Deepesh Garg
5690e9771a Merge pull request #32256 from frappe/mergify/bp/version-13-hotfix/pr-32244
fix: Parent Level project linkning on creating PO from project (backport #32244)
2022-09-18 19:41:57 +05:30
Sagar Sharma
ec6cac8043 chore: conflicts 2022-09-18 16:38:09 +05:30
Sagar Sharma
0329642116 fix: make po_detail or sco_rm_detail mandatory for SE Send to Subcontractor
(cherry picked from commit b90875575c)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2022-09-18 09:25:10 +00:00
Deepesh Garg
3c3e3cfcf8 fix: Parent Level project linkning on creating PO from project
(cherry picked from commit 93e134aab0)
2022-09-17 15:00:56 +00:00
Sagar Sharma
7c4f5fa5c5 Merge pull request #32241 from frappe/mergify/bp/version-13-hotfix/pr-32236
fix: production plan pending-qty (backport #32236)
2022-09-16 20:43:28 +05:30
Sagar Sharma
367e05f808 test: update test case for production plan pending-qty
(cherry picked from commit bd6af7c613)
2022-09-16 13:00:59 +00:00
Sagar Sharma
2dcb35da33 fix: production plan pending-qty
(cherry picked from commit 5be7d42dfd)
2022-09-16 13:00:58 +00:00
Sagar Sharma
94732479f5 Merge pull request #32237 from frappe/mergify/bp/version-13-hotfix/pr-32233
refactor: rewrite Production Plan queries in QB (backport #32233)
2022-09-16 17:35:38 +05:30
Sagar Sharma
b4dec4630d chore: conflicts 2022-09-16 17:08:35 +05:30
Sagar Sharma
c415a47d25 refactor: rewrite Production Plan queries in QB
(cherry picked from commit b8cf3b4c77)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.py
2022-09-16 09:30:11 +00:00
rohitwaghchaure
1e0cc65c61 Merge pull request #32220 from frappe/mergify/bp/version-13-hotfix/pr-31681
fix: dont show zero qty available items in stock ageing report (backport #31681)
2022-09-15 12:39:22 +05:30
Rohit Waghchaure
37dbc7043a fix: dont show zero qty available items in stock ageing report
(cherry picked from commit 5da7e01db2)
2022-09-15 06:01:10 +00:00
Deepesh Garg
0e88496607 Merge pull request #32209 from frappe/mergify/bp/version-13-hotfix/pr-32208
fix: Loans pending accrual entries (backport #32208)
2022-09-14 15:34:10 +05:30
Abhinav Raut
ef86b437cb fix: pending accrual entries
(cherry picked from commit f2209045f8)
2022-09-14 08:45:27 +00:00
Deepesh Garg
b7b0076743 Merge pull request #32194 from deepeshgarg007/loan_amount_post_write_off
fix: Loan amount post write off
2022-09-13 13:41:21 +05:30
Deepesh Garg
ef5dd1d693 Merge pull request #32165 from frappe/mergify/bp/version-13-hotfix/pr-32144
fix: Rate for internal PI have non stock UOM items (backport #32144)
2022-09-13 12:02:12 +05:30
Deepesh Garg
a53b40ba93 chore: Remove print 2022-09-13 12:00:06 +05:30
Deepesh Garg
e78a7679a4 fix: Loan amount post write off 2022-09-13 11:53:11 +05:30
Deepesh Garg
6a5beecb36 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-32144 2022-09-13 09:39:37 +05:30
Rucha Mahabal
2b900e2f0e Merge pull request #32187 from ruchamahabal/stat-comp-pd
fix: calculate amount based on payment days for statistical components too
2022-09-13 02:09:39 +05:30
Rucha Mahabal
bc14dbbcad refactor(tests): add a test utility file for commonly used functions in tests 2022-09-13 01:42:27 +05:30
Rucha Mahabal
f4e53a5c91 test: statistical component based on payment days 2022-09-13 00:59:59 +05:30
Rucha Mahabal
3f99522764 fix: calculate amount based on payment days for statistical components too 2022-09-13 00:59:03 +05:30
Sagar Sharma
81dd9722fe Merge pull request #32179 from Havenir/fix-picklist-picked-qty
fix: pick_list - picked qty getting set to 1
2022-09-12 22:19:26 +05:30
Sagar Sharma
9c5d335360 Merge pull request #32186 from frappe/mergify/bp/version-13-hotfix/pr-32150
refactor: BOM Stock Calculated report, fix required-qty (backport #32150)
2022-09-12 19:48:38 +05:30
Sagar Sharma
c5c28615f5 test: add test cases for BOM Stock Calculated report
(cherry picked from commit e1a98c1ff7)
2022-09-12 13:52:31 +00:00
Sagar Sharma
ab32c09ff9 fix: add missing warehouse filter in BOM Stock Calculated report
(cherry picked from commit 7a968a5f0d)
2022-09-12 13:52:30 +00:00
Sagar Sharma
f2e63bc491 fix: required_qty in BOM Stock Calculated report
(cherry picked from commit 56192daabf)
2022-09-12 13:52:30 +00:00
Sagar Sharma
882aa96973 refactor: BOM Stock Calculated report
(cherry picked from commit 723fa9eebc)
2022-09-12 13:52:29 +00:00
Rucha Mahabal
d29a033c09 fix(Salary Slip): set default amount to 0 if None (#32184) 2022-09-12 19:21:58 +05:30
Deepesh Garg
b3125a56ed Merge pull request #32178 from deepeshgarg007/manual_update_loan_amount
feat: Ability to manually update loan amount in Salary Slips
2022-09-12 17:41:02 +05:30
rohitwaghchaure
adfc57487b Merge pull request #32167 from frappe/mergify/bp/version-13-hotfix/pr-32118
fix: option to start reposting from repost item valuation (backport #32118)
2022-09-12 16:16:26 +05:30
Deepesh Garg
ac320e4d55 feat: Ability to manually update loan amount in Salary Slips 2022-09-12 15:05:42 +05:30
Ahmad
3256e2b8b7 fix: pick_list - picked qty getting set to 1 2022-09-12 14:23:44 +05:00
mergify[bot]
b4ec4ccc56 chore: correct license text for GPLv3 (backport #32170) (#32172)
* chore: correct license text for GPLv3 (#32170)

[skip ci]



Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-09-12 14:00:14 +05:30
Rohit Waghchaure
a1826f215a fix: option to start reposting from repost item valuation
(cherry picked from commit f1c4aea7b5)
2022-09-12 06:44:59 +00:00
Deepesh Garg
181c0acf99 Merge pull request #32127 from SolufyPrivateLimited/Solufy-so-to-po-v13
fix: Purchase Order creation from Sales Order
2022-09-12 09:25:24 +05:30
Deepesh Garg
638d6b7177 fix: Rate for internal PI have non stock UOM items
(cherry picked from commit 0f655e4430)
2022-09-12 03:40:23 +00:00
rohitwaghchaure
f5132411eb Merge pull request #32152 from frappe/mergify/bp/version-13-hotfix/pr-32135
fix: reposting not working for internal transferred purchase receipt (backport #32135)
2022-09-10 23:29:56 +05:30
rohitwaghchaure
e177f1e51b fix: linter 2022-09-10 23:01:48 +05:30
rohitwaghchaure
6f2a567c95 fix: conflict 2022-09-10 16:41:39 +05:30
Rohit Waghchaure
bb41d8bc47 fix: reposting not working for internal transferred purchase receipt
(cherry picked from commit a03b4ce213)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2022-09-09 17:13:21 +00:00
Nihantra C. Patel
d79aacd1cb fix: Purchase Order creation from Sales Order 2022-09-08 13:51:11 +05:30
Sagar Sharma
44c3a322d3 fix: require barcode item barcode (#32112)
fix: require barcode item barcode. (#31957)

* fix: require barcode item barcode.

* fix: make supplier mandatory in Item Supplier DocType

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2022-09-07 14:24:53 +05:30
150 changed files with 4185 additions and 2620 deletions

View File

@@ -93,7 +93,7 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 2 --build-number ${{ matrix.container }}'
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}

View File

@@ -12,17 +12,13 @@ erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
erpnext/e_commerce/ @marination
erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
erpnext/portal/ @marination
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/healthcare/ @chillaranand
erpnext/hr/ @ruchamahabal
erpnext/non_profit/ @ruchamahabal
@@ -30,7 +26,7 @@ erpnext/payroll @ruchamahabal
erpnext/projects/ @ruchamahabal
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/public/ @nextchamp-saqib @marination
.github/ @ankush

View File

@@ -65,6 +65,8 @@ GNU/General Public License (see [license.txt](license.txt))
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
---
## Contributing

View File

@@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", {
var update_jv_details = function(doc, r) {
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
row.account = d.account;
row.balance = d.balance;
frappe.model.set_value(row.doctype, row.name, "account", d.account)
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
});
refresh_field("accounts");
}

View File

@@ -194,7 +194,9 @@ class JournalEntry(AccountsController):
}
)
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return

View File

@@ -1111,7 +1111,7 @@ frappe.ui.form.on('Payment Entry', {
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
});
},
@@ -1202,7 +1202,7 @@ frappe.ui.form.on('Payment Entry', {
}
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
});
},
@@ -1234,6 +1234,7 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.total_taxes_and_charges = 0.0;
frm.doc.base_total_taxes_and_charges = 0.0;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let actual_tax_dict = {};
// maintain actual tax rate based on idx
@@ -1254,8 +1255,8 @@ frappe.ui.form.on('Payment Entry', {
}
}
tax.tax_amount = current_tax_amount;
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
// tax accounts are only in company currency
tax.base_tax_amount = current_tax_amount;
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
if(i==0) {
@@ -1264,9 +1265,29 @@ frappe.ui.form.on('Payment Entry', {
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
}
tax.base_total = tax.total * frm.doc.source_exchange_rate;
frm.doc.total_taxes_and_charges += current_tax_amount;
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
// tac accounts are only in company currency
tax.base_total = tax.total
// calculate total taxes and base total taxes
if(frm.doc.payment_type == "Pay") {
// tax accounts only have company currency
if(tax.currency != frm.doc.paid_to_account_currency) {
//total_taxes_and_charges has the target currency. so using target conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
} else if(frm.doc.payment_type == "Receive") {
if(tax.currency != frm.doc.paid_from_account_currency) {
//total_taxes_and_charges has the target currency. so using source conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
}
frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
frm.refresh_field('taxes');
frm.refresh_field('total_taxes_and_charges');

View File

@@ -944,6 +944,13 @@ class PaymentEntry(AccountsController):
)
if not d.included_in_paid_amount:
if get_account_currency(payment_account) != self.company_currency:
if self.payment_type == "Receive":
exchange_rate = self.target_exchange_rate
elif self.payment_type in ["Pay", "Internal Transfer"]:
exchange_rate = self.source_exchange_rate
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
gl_entries.append(
self.get_gl_dict(
{
@@ -1059,7 +1066,7 @@ class PaymentEntry(AccountsController):
for fieldname in tax_fields:
tax.set(fieldname, 0.0)
self.paid_amount_after_tax = self.paid_amount
self.paid_amount_after_tax = self.base_paid_amount
def determine_exclusive_rate(self):
if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
@@ -1078,7 +1085,7 @@ class PaymentEntry(AccountsController):
cumulated_tax_fraction += tax.tax_fraction_for_current_item
self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
@@ -1101,7 +1108,7 @@ class PaymentEntry(AccountsController):
current_tax_amount += actual_tax_dict[tax.idx]
tax.tax_amount = current_tax_amount
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
tax.base_tax_amount = current_tax_amount
if tax.add_deduct_tax == "Deduct":
current_tax_amount *= -1.0
@@ -1115,14 +1122,20 @@ class PaymentEntry(AccountsController):
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
)
tax.base_total = tax.total * self.source_exchange_rate
tax.base_total = tax.total
if self.payment_type == "Pay":
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if tax.currency != self.paid_to_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
elif self.payment_type == "Receive":
if tax.currency != self.paid_from_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe import qb
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -743,6 +744,46 @@ class TestPaymentEntry(unittest.TestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_gl_of_multi_currency_payment_with_taxes(self):
payment_entry = create_payment_entry(
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "Actual",
"tax_amount": 100,
"add_deduct_tax": "Add",
"description": "Test",
},
)
payment_entry.target_exchange_rate = 80
payment_entry.received_amount = 12.5
payment_entry = payment_entry.submit()
gle = qb.DocType("GL Entry")
gl_entries = (
qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
gle.debit_in_account_currency,
gle.credit_in_account_currency,
)
.orderby(gle.account)
.where(gle.voucher_no == payment_entry.name)
.run()
)
expected_gl_entries = (
("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
)
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()

View File

@@ -8,7 +8,11 @@ from frappe.model.document import Document
from frappe.utils import flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
from erpnext.accounts.utils import (
get_outstanding_invoices,
reconcile_against_document,
update_reference_in_payment_entry,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -190,6 +194,23 @@ class PaymentReconciliation(Document):
inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, allocated_entry):
if allocated_entry.get("reference_type") != "Payment Entry":
return
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
row = self.get_payment_details(allocated_entry, dr_or_cr)
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
update_reference_in_payment_entry(row, doc, do_not_save=True)
return doc.difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
@@ -205,12 +226,16 @@ class PaymentReconciliation(Document):
res = self.get_allocated_entry(pay, inv, pay["amount"])
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0
res.difference_amount = self.get_difference_amount(res)
if pay.get("amount") == 0:
entries.append(res)
break
elif inv.get("outstanding_amount") == 0:
entries.append(res)
continue
else:
break
@@ -332,7 +357,7 @@ class PaymentReconciliation(Document):
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
if self.get("cost_center"):
condition = " and cost_center = '{0}' ".format(self.cost_center)
if get_invoices:

View File

@@ -186,8 +186,10 @@
{
"fetch_from": "bank_account.bank",
"fieldname": "bank",
"fieldtype": "Read Only",
"label": "Bank"
"fieldtype": "Link",
"label": "Bank",
"options": "Bank",
"read_only": 1
},
{
"fetch_from": "bank_account.bank_account_no",
@@ -366,10 +368,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-18 12:24:14.178853",
"modified": "2022-09-30 16:19:43.680025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -401,5 +404,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -39,6 +39,7 @@
{
"columns": 2,
"fetch_from": "payment_term.description",
"fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@@ -159,7 +160,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-28 05:41:35.084233",
"modified": "2022-09-16 13:57:06.382859",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -168,5 +169,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1572,7 +1572,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2022-03-22 13:00:24.166684",
"modified": "2022-09-27 13:00:24.166684",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if flt(available_stock) <= 0:
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif flt(available_stock) < flt(d.qty):
elif is_stock_item and flt(available_stock) < flt(d.qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -634,11 +634,12 @@ def get_stock_availability(item_code, warehouse):
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = False
is_stock_item = True
if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
@@ -652,7 +653,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles:
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
@@ -744,3 +747,7 @@ def add_return_modes(doc, pos_profile):
]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
def on_doctype_update():
frappe.db.add_index("POS Invoice", ["return_against"])

View File

@@ -495,6 +495,67 @@ class TestPOSInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pos.submit)
def test_value_error_on_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(
company="_Test Company",
target_warehouse="Stores - _TC",
cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC",
)
serial_nos = se.get("items")[0].serial_no
# make a pos invoice
pos = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=1,
do_not_save=1,
)
pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
pos.set("payments", [])
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
)
pos = pos.save().submit()
# make a return
pos_return = make_sales_return(pos.name)
pos_return.paid_amount = pos_return.grand_total
pos_return.save()
pos_return.submit()
# set docstatus to 2 for pos to trigger this issue
frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2)
pos2 = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=1,
do_not_save=1,
)
pos2.get("items")[0].has_serial_no = 1
pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
# Value error should not be triggered on validation
pos2.save()
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,

View File

@@ -269,6 +269,18 @@ def get_serial_no_for_item(args):
return item_details
def update_pricing_rule_uom(pricing_rule, args):
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
pricing_rule.apply_on
)
apply_on_field = frappe.scrub(pricing_rule.apply_on)
for row in pricing_rule.get(child_doc):
if row.get(apply_on_field) == args.get(apply_on_field):
pricing_rule.uom = row.uom
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
@@ -325,7 +337,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if isinstance(pricing_rule, string_types):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
update_pricing_rule_uom(pricing_rule, args)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
if pricing_rule.get("suggestion"):
continue
@@ -439,12 +452,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
# TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way.
if pricing_rule_rate:
is_blank_uom = pricing_rule.get("uom") != args.get("uom")
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update(
{
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"price_list_rate": pricing_rule_rate
* (args.get("conversion_factor", 1) if is_blank_uom else 1),
}
)
item_details.update({"discount_percentage": 0.0})

View File

@@ -597,6 +597,121 @@ class TestPricingRule(unittest.TestCase):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_item_price_with_blank_uom_pricing_rule(self):
properties = {
"item_code": "Item Blank UOM",
"stock_uom": "Nos",
"sales_uom": "Box",
"uoms": [dict(uom="Box", conversion_factor=10)],
}
item = make_item(properties=properties)
make_item_price("Item Blank UOM", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Item Blank UOM Rule",
"apply_on": "Item Code",
"items": [
{
"item_code": "Item Blank UOM",
}
],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 101,
"company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(
do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10
)
si.selling_price_list = "_Test Price List"
si.save()
# If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
# rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
self.assertEqual(si.items[0].price_list_rate, 1010)
self.assertEqual(si.items[0].rate, 1010)
si.delete()
si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos")
si.selling_price_list = "_Test Price List"
si.save()
# UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
# rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
self.assertEqual(si.items[0].price_list_rate, 101)
self.assertEqual(si.items[0].rate, 101)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete()
item.delete()
def test_item_price_with_selling_uom_pricing_rule(self):
properties = {
"item_code": "Item UOM other than Stock",
"stock_uom": "Nos",
"sales_uom": "Box",
"uoms": [dict(uom="Box", conversion_factor=10)],
}
item = make_item(properties=properties)
make_item_price("Item UOM other than Stock", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Item UOM other than Stock Rule",
"apply_on": "Item Code",
"items": [
{
"item_code": "Item UOM other than Stock",
"uom": "Box",
}
],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 101,
"company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(
do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10
)
si.selling_price_list = "_Test Price List"
si.save()
# UOM is Box so apply pricing_rule only on Box UOM.
# Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
self.assertEqual(si.items[0].price_list_rate, 101)
self.assertEqual(si.items[0].rate, 101)
si.delete()
si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos")
si.selling_price_list = "_Test Price List"
si.save()
# UOM is Box so pricing_rule won't apply as selling_uom is Nos.
# As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].rate, 100)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete()
item.delete()
def test_pricing_rule_for_different_currency(self):
make_item("Test Sanitizer Item")

View File

@@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values):
)
if apply_on_field == "item_code":
if args.get("uom", None):
item_conditions += (
" and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
child_doc=child_doc, item_uom=args.get("uom")
)
)
if "variant_of" not in args:
args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of")

View File

@@ -25,7 +25,7 @@
</div>
<br>
<table class="table table-bordered">
<table class="table table-bordered" style="font-size: 10px">
<thead>
<tr>
<th style="width: 12%">{{ _("Date") }}</th>

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this._super();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
if (frm.is_new()) {
frm.clear_table("tax_withheld_vouchers");
}
},
is_subcontracted: function(frm) {

View File

@@ -83,6 +83,8 @@
"section_break_51",
"taxes_and_charges",
"taxes",
"tax_withheld_vouchers_section",
"tax_withheld_vouchers",
"sec_tax_breakup",
"other_charges_calculation",
"totals",
@@ -511,7 +513,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1417,13 +1418,26 @@
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
},
{
"fieldname": "tax_withheld_vouchers_section",
"fieldtype": "Section Break",
"label": "Tax Withheld Vouchers"
},
{
"fieldname": "tax_withheld_vouchers",
"fieldtype": "Table",
"label": "Tax Withheld Vouchers",
"no_copy": 1,
"options": "Tax Withheld Vouchers",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-11-25 13:31:02.716727",
"modified": "2022-10-07 14:19:14.214157",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1483,7 +1497,8 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -67,6 +67,9 @@ class PurchaseInvoice(BuyingController):
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
if self.is_new():
self.set("tax_withheld_vouchers", [])
def before_save(self):
if not self.on_hold:
self.release_date = ""
@@ -695,6 +698,10 @@ class PurchaseInvoice(BuyingController):
)
)
credit_amount = item.base_net_amount
if self.is_internal_supplier and item.valuation_rate:
credit_amount = flt(item.valuation_rate * item.stock_qty)
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
gl_entries.append(
self.get_gl_dict(
@@ -704,7 +711,7 @@ class PurchaseInvoice(BuyingController):
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
},
warehouse_account[item.from_warehouse]["account_currency"],
item=item,
@@ -1364,7 +1371,14 @@ class PurchaseInvoice(BuyingController):
frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Tax Withheld Vouchers",
)
self.update_advance_tax_references(cancel=1)
def update_project(self):
@@ -1457,7 +1471,7 @@ class PurchaseInvoice(BuyingController):
if not self.tax_withholding_category:
return
tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
self, self.tax_withholding_category
)
@@ -1486,6 +1500,19 @@ class PurchaseInvoice(BuyingController):
for d in to_remove:
self.remove(d)
## Add pending vouchers on which tax was withheld
self.set("tax_withheld_vouchers", [])
for voucher_no, voucher_details in voucher_wise_amount.items():
self.append(
"tax_withheld_vouchers",
{
"voucher_name": voucher_no,
"voucher_type": voucher_details.get("voucher_type"),
"taxable_amount": voucher_details.get("amount"),
},
)
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()

View File

@@ -1549,6 +1549,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000)
def test_batch_expiry_for_purchase_invoice(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
item = self.make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pi = make_purchase_invoice(
qty=1,
item_code=item.name,
update_stock=True,
)
pi.load_from_db()
batch_no = pi.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save().submit()
self.assertTrue(return_pi.docstatus == 1)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -706,6 +706,7 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -871,7 +872,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-11-15 17:04:07.191013",
"modified": "2022-10-12 03:37:29.032732",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -651,7 +651,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
"print_hide": 1
},
{
@@ -2046,7 +2045,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-07-11 17:43:56.435382",
"modified": "2022-09-16 17:44:22.227332",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -2187,6 +2187,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
)
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
)
if currency:
target_doc.currency = currency

View File

@@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.utils import add_days, flt, getdate, nowdate
from frappe.utils import add_days, flt, getdate, nowdate, today
from six import iteritems
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -31,10 +31,20 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
get_qty_after_transaction,
make_stock_entry,
)
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
class TestSalesInvoice(unittest.TestCase):
def setUp(self):
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
create_internal_parties()
setup_accounts()
def make(self):
w = frappe.copy_doc(test_records[0])
w.is_pos = 0
@@ -906,7 +916,8 @@ class TestSalesInvoice(unittest.TestCase):
pos_return.insert()
pos_return.submit()
self.assertEqual(pos_return.get("payments")[0].amount, -1000)
self.assertEqual(pos_return.get("payments")[0].amount, -500)
self.assertEqual(pos_return.get("payments")[1].amount, -500)
def test_pos_change_amount(self):
make_pos_profile(
@@ -1687,7 +1698,7 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
def test_outstanding_amount_after_advance_jv_cancelation(self):
def test_outstanding_amount_after_advance_jv_cancellation(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
)
@@ -1731,7 +1742,7 @@ class TestSalesInvoice(unittest.TestCase):
flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
)
def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -2369,29 +2380,6 @@ class TestSalesInvoice(unittest.TestCase):
acc_settings.save()
def test_inter_company_transaction(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
create_internal_customer(
customer_name="_Test Internal Customer",
represents_company="_Test Company 1",
allowed_to_interact_with="Wind Power LLC",
)
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
supplier = frappe.get_doc(
{
"supplier_group": "_Test Supplier Group",
"supplier_name": "_Test Internal Supplier",
"doctype": "Supplier",
"is_internal_supplier": 1,
"represents_company": "Wind Power LLC",
}
)
supplier.append("companies", {"company": "_Test Company 1"})
supplier.insert()
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
@@ -2451,34 +2439,9 @@ class TestSalesInvoice(unittest.TestCase):
se.cancel()
def test_internal_transfer_gl_entry(self):
## Create internal transfer account
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
account = create_account(
account_name="Unrealized Profit",
parent_account="Current Liabilities - TCP1",
company="_Test Company with perpetual inventory",
)
frappe.db.set_value(
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
)
customer = create_internal_customer(
"_Test Internal Customer 2",
"_Test Company with perpetual inventory",
"_Test Company with perpetual inventory",
)
create_internal_supplier(
"_Test Internal Supplier 2",
"_Test Company with perpetual inventory",
"_Test Company with perpetual inventory",
)
si = create_sales_invoice(
company="_Test Company with perpetual inventory",
customer=customer,
customer="_Test Internal Customer 2",
debit_to="Debtors - TCP1",
warehouse="Stores - TCP1",
income_account="Sales - TCP1",
@@ -2492,7 +2455,7 @@ class TestSalesInvoice(unittest.TestCase):
si.update_stock = 1
si.items[0].target_warehouse = "Work In Progress - TCP1"
# Add stock to stores for succesful stock transfer
# Add stock to stores for successful stock transfer
make_stock_entry(
target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100
)
@@ -2830,6 +2793,77 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
def test_internal_transfer_gl_precision_issues(self):
# Make a stock queue of an item with two valuations
# Remove all existing stock for this
if get_stock_balance("_Test Internal Transfer Item", "Stores - TCP1", "2022-04-10"):
create_stock_reconciliation(
item_code="_Test Internal Transfer Item",
warehouse="Stores - TCP1",
qty=0,
rate=0,
company="_Test Company with perpetual inventory",
expense_account="Stock Adjustment - TCP1"
if frappe.get_all("Stock Ledger Entry")
else "Temporary Opening - TCP1",
posting_date="2020-04-10",
posting_time="14:00",
)
make_stock_entry(
item_code="_Test Internal Transfer Item",
target="Stores - TCP1",
qty=9000000,
basic_rate=52.0,
posting_date="2020-04-10",
posting_time="14:00",
)
make_stock_entry(
item_code="_Test Internal Transfer Item",
target="Stores - TCP1",
qty=60000000,
basic_rate=52.349777,
posting_date="2020-04-10",
posting_time="14:00",
)
# Make an internal transfer Sales Invoice Stock in non stock uom to check
# for rounding errors while converting to stock uom
si = create_sales_invoice(
company="_Test Company with perpetual inventory",
customer="_Test Internal Customer 2",
item_code="_Test Internal Transfer Item",
qty=5000000,
uom="Box",
debit_to="Debtors - TCP1",
warehouse="Stores - TCP1",
income_account="Sales - TCP1",
expense_account="Cost of Goods Sold - TCP1",
cost_center="Main - TCP1",
currency="INR",
do_not_save=1,
)
# Check GL Entries with precision
si.update_stock = 1
si.items[0].target_warehouse = "Work In Progress - TCP1"
si.items[0].conversion_factor = 10
si.save()
si.submit()
# Check if adjustment entry is created
self.assertTrue(
frappe.db.exists(
"GL Entry",
{
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
"remarks": "Rounding gain/loss Entry for Stock Transfer",
},
)
)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
@@ -3278,7 +3312,7 @@ class TestSalesInvoice(unittest.TestCase):
[deferred_account, 2022.47, 0.0, "2019-03-15"],
]
gl_entries = gl_entries = frappe.db.sql(
gl_entries = frappe.db.sql(
"""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
@@ -3396,6 +3430,37 @@ class TestSalesInvoice(unittest.TestCase):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
)
def test_batch_expiry_for_sales_invoice_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pr = make_purchase_receipt(qty=1, item_code=item.name)
batch_no = pr.items[0].batch_no
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
si.load_from_db()
batch_no = si.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_si = make_return_doc(si.doctype, si.name)
return_si.save().submit()
self.assertTrue(return_si.docstatus == 1)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
@@ -3652,6 +3717,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item",
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse,
"qty": args.qty or 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
@@ -3664,8 +3730,9 @@ def create_sales_invoice(**args):
"discount_amount": args.discount_amount or 0,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": 1,
"conversion_factor": args.get("conversion_factor", 1),
"incoming_rate": args.incoming_rate or 0,
"batch_no": args.batch_no or None,
},
)
@@ -3777,6 +3844,34 @@ def get_taxes_and_charges():
]
def create_internal_parties():
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
create_internal_customer(
customer_name="_Test Internal Customer",
represents_company="_Test Company 1",
allowed_to_interact_with="Wind Power LLC",
)
create_internal_customer(
customer_name="_Test Internal Customer 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier",
represents_company="Wind Power LLC",
allowed_to_interact_with="_Test Company 1",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.get_doc(
@@ -3799,6 +3894,19 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
return supplier_name
def setup_accounts():
## Create internal transfer account
account = create_account(
account_name="Unrealized Profit",
parent_account="Current Liabilities - TCP1",
company="_Test Company with perpetual inventory",
)
frappe.db.set_value(
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
)
def add_taxes(doc):
doc.append(
"taxes",

View File

@@ -812,6 +812,8 @@
"fieldtype": "Currency",
"label": "Incoming Rate (Costing)",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1
},
{
@@ -841,7 +843,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-08-26 12:06:31.205417",
"modified": "2022-10-10 20:57:38.340026",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -0,0 +1,49 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2022-09-13 16:18:59.404842",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_name",
"taxable_amount"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Voucher Name",
"options": "voucher_type"
},
{
"fieldname": "taxable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Taxable Amount",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-09-13 23:40:41.479208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TaxWithheldVouchers(Document):
pass

View File

@@ -100,7 +100,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
).format(tax_withholding_category, inv.company, party)
)
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
party_type, parties, inv, tax_details, posting_date, pan_no
)
@@ -110,7 +110,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
@@ -208,7 +208,9 @@ def get_lower_deduction_certificate(tax_details, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
vouchers, voucher_wise_amount = get_invoice_vouchers(
parties, tax_details, inv.company, party_type=party_type
)
advance_vouchers = get_advance_vouchers(
parties,
company=inv.company,
@@ -227,6 +229,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
@@ -237,6 +240,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
)
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
else:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
@@ -252,12 +258,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
dr_or_cr = "credit" if party_type == "Supplier" else "debit"
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
voucher_wise_amount = {}
vouchers = []
filters = {
"company": company,
@@ -272,29 +279,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
)
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"])
journal_entries = frappe.db.sql(
for d in invoices_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
journal_entries_details = frappe.db.sql(
"""
SELECT j.name
SELECT j.name, ja.credit - ja.debit AS amount
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.docstatus = 1
j.name = ja.parent
AND j.docstatus = 1
AND j.is_opening = 'No'
AND j.posting_date between %s and %s
AND ja.{dr_or_cr} > 0
AND ja.party in %s
""".format(
dr_or_cr=dr_or_cr
AND j.apply_tds = 1
AND j.tax_withholding_category = %s
""",
(
tax_details.from_date,
tax_details.to_date,
tuple(parties),
tax_details.get("tax_withholding_category"),
),
(tax_details.from_date, tax_details.to_date, tuple(parties)),
as_list=1,
as_dict=1,
)
if journal_entries:
journal_entries = journal_entries[0]
if journal_entries_details:
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
return invoices + journal_entries
return vouchers, voucher_wise_amount
def get_advance_vouchers(
@@ -311,6 +329,9 @@ def get_advance_vouchers(
"party": ["in", parties],
}
if party_type == "Customer":
filters.update({"against_voucher": ["is", "not set"]})
if company:
filters["company"] = company
if from_date and to_date:
@@ -320,23 +341,25 @@ def get_advance_vouchers(
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
advances = [d.reference_name for d in inv.get("advances")]
tax_info = []
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
if inv.get("advances"):
advances = [d.reference_name for d in inv.get("advances")]
tax_info = (
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
.run(as_dict=True)
)
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
tax_info = (
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
.run(as_dict=True)
)
return tax_info
@@ -358,6 +381,9 @@ def get_deducted_tax(taxable_vouchers, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0
supp_credit_amt = 0.0
supp_jv_credit_amt = 0.0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
field = "sum(net_total)"
@@ -366,30 +392,25 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
invoice_filters.pop("apply_tds", None)
field = "sum(grand_total)"
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
if vouchers:
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) 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_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, tax_details.from_date, tax_details.to_date, inv.company
)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
@@ -401,7 +422,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0
net_total = 0
if vouchers:
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)")
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
@@ -422,36 +446,40 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
invoiced_amt = 0
advance_amt = 0
# 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)",
if vouchers:
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
)
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)",
if advance_amt:
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
)
or 0.0
)
# sum of credit entries made from sales invoice
credit_note_amt = sum(
@@ -506,22 +534,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
return tds_amount
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
filters = {
"supplier": ["in", suppliers],
"is_return": 1,
"docstatus": 1,
"posting_date": ["between", (from_date, to_date)],
}
fields = ["abs(sum(net_total)) as net_total"]
if company:
filters["company"] = company
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):
return current_amount * rate / 100

View File

@@ -52,7 +52,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
invoices.append(pi)
# delete invoices to avoid clashing
for d in invoices:
for d in reversed(invoices):
d.cancel()
def test_single_threshold_tds(self):
@@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(pi.taxes_and_charges_deducted, 1000)
# delete invoices to avoid clashing
for d in invoices:
for d in reversed(invoices):
d.cancel()
def test_tax_withholding_category_checks(self):
@@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in invoices:
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_tcs(self):
@@ -148,8 +148,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(tcs_charged, 500)
invoices.append(si)
# delete invoices to avoid clashing
for d in invoices:
# cancel invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_tds_calculation_on_net_total(self):
@@ -182,8 +182,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
# delete invoices to avoid clashing
for d in invoices:
# cancel invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_multi_category_single_supplier(self):
@@ -207,8 +207,50 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(pi1.taxes[0].tax_amount, 250)
# delete invoices to avoid clashing
for d in invoices:
# cancel invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_tax_withholding_category_voucher_display(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category"
)
invoices = []
pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True)
pi.apply_tds = 1
pi.tax_withholding_category = "Test Multi Invoice Category"
pi.save()
pi.submit()
invoices.append(pi)
pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True)
pi1.apply_tds = 1
pi1.is_return = 1
pi1.items[0].qty = -1
pi1.tax_withholding_category = "Test Multi Invoice Category"
pi1.save()
pi1.submit()
invoices.append(pi1)
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
pi2.apply_tds = 1
pi2.tax_withholding_category = "Test Multi Invoice Category"
pi2.save()
pi2.submit()
invoices.append(pi2)
pi2.load_from_db()
self.assertTrue(pi2.taxes[0].tax_amount, 1100)
self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name)
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
# cancel invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
@@ -308,6 +350,7 @@ def create_records():
"Test TDS Supplier3",
"Test TDS Supplier4",
"Test TDS Supplier5",
"Test TDS Supplier6",
]:
if frappe.db.exists("Supplier", name):
continue
@@ -498,3 +541,22 @@ def create_tax_with_holding_category():
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"):
frappe.get_doc(
{
"doctype": "Tax Withholding Category",
"name": "Test Multi Invoice Category",
"category_name": "Test Multi Invoice Category",
"rates": [
{
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 5000,
"cumulative_threshold": 10000,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()

View File

@@ -50,6 +50,10 @@
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Date</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Dt }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">

View File

@@ -280,9 +280,9 @@ def get_conditions(filters):
or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"]
):
conditions.append("posting_date >=%(from_date)s")
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s)")
if filters.get("project"):
conditions.append("project in %(project)s")

View File

@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
for d in data:
for period in period_list:
key = period if consolidated else period.key
d[key] = totals[d["account"]]
d["total"] = totals[d["account"]]
return data

View File

@@ -19,14 +19,19 @@ def execute(filters=None):
return _execute(filters)
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
def _execute(
filters=None,
additional_table_columns=None,
additional_query_columns=None,
additional_conditions=None,
):
if not filters:
filters = {}
columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_query_columns)
item_list = get_items(filters, additional_query_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -97,6 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row.update({"rate": d.base_net_rate, "amount": d.base_net_amount})
total_tax = 0
total_other_charges = 0
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
@@ -105,10 +111,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
}
)
total_tax += flt(item_tax.get("tax_amount"))
if item_tax.get("is_other_charges"):
total_other_charges += flt(item_tax.get("tax_amount"))
else:
total_tax += flt(item_tax.get("tax_amount"))
row.update(
{"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency}
{
"total_tax": total_tax,
"total_other_charges": total_other_charges,
"total": d.base_net_amount + total_tax,
"currency": company_currency,
}
)
if filters.get("group_by"):
@@ -319,7 +333,7 @@ def get_columns(additional_table_columns, filters):
return columns
def get_conditions(filters):
def get_conditions(filters, additional_conditions=None):
conditions = ""
for opts in (
@@ -332,6 +346,9 @@ def get_conditions(filters):
if filters.get(opts[0]):
conditions += opts[1]
if additional_conditions:
conditions += additional_conditions
if filters.get("mode_of_payment"):
conditions += """ and exists(select name from `tabSales Invoice Payment`
where parent=`tabSales Invoice`.name
@@ -367,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters, additional_conditions)
if additional_query_columns:
additional_query_columns = ", " + ", ".join(additional_query_columns)
@@ -477,7 +494,7 @@ def get_tax_accounts(
tax_details = frappe.db.sql(
"""
select
name, parent, description, item_wise_tax_detail,
name, parent, description, item_wise_tax_detail, account_head,
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
from `tab%s`
where
@@ -493,11 +510,22 @@ def get_tax_accounts(
tuple([doctype] + list(invoice_item_row)),
)
account_doctype = frappe.qb.DocType("Account")
query = (
frappe.qb.from_(account_doctype)
.select(account_doctype.name)
.where((account_doctype.account_type == "Tax"))
)
tax_accounts = query.run()
for (
name,
parent,
description,
item_wise_tax_detail,
account_head,
charge_type,
add_deduct_tax,
tax_amount,
@@ -540,7 +568,11 @@ def get_tax_accounts(
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
{"tax_rate": tax_rate, "tax_amount": tax_value}
{
"tax_rate": tax_rate,
"tax_amount": tax_value,
"is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1,
}
)
except ValueError:
@@ -583,6 +615,13 @@ def get_tax_accounts(
"options": "currency",
"width": 100,
},
{
"label": _("Total Other Charges"),
"fieldname": "total_other_charges",
"fieldtype": "Currency",
"options": "currency",
"width": 100,
},
{
"label": _("Total"),
"fieldname": "total",

View File

@@ -370,7 +370,7 @@ def get_conditions(filters):
where parent=`tabSales Invoice`.name
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("cost_center")
conditions += get_sales_invoice_item_field_condition("warehouse")
conditions += get_sales_invoice_item_field_condition("brand")

View File

@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
query_filters = {
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"report_type": report_type,
"year_start_date": filters.year_start_date,
"project": filters.project,
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
where
company=%(company)s
{additional_conditions}
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
and account in (select name from `tabAccount` where report_type=%(report_type)s)
and is_cancelled = 0
group by account""".format(

View File

@@ -106,12 +106,17 @@ def get_opening_balances(filters):
where company=%(company)s
and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
{account_filter}
group by party""".format(
account_filter=account_filter
),
{"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type},
{
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"party_type": filters.party_type,
},
as_dict=True,
)

View File

@@ -818,6 +818,28 @@ def get_held_invoices(party_type, party):
return held_invoices
def remove_return_pos_invoices(party_type, party, invoice_list):
if invoice_list:
if party_type == "Customer":
sinv = frappe.qb.DocType("Sales Invoice")
return_pos = (
frappe.qb.from_(sinv)
.select(sinv.name)
.where((sinv.is_pos == 1) & (sinv.docstatus == 1) & (sinv.is_return == 1))
.run()
)
if return_pos:
return_pos = [x[0] for x in return_pos]
else:
return invoice_list
invoice_list = [x for x in invoice_list if x.voucher_no not in return_pos]
return invoice_list
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
@@ -868,6 +890,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
as_dict=True,
)
invoice_list = remove_return_pos_invoices(party_type, party, invoice_list)
payment_entries = frappe.db.sql(
"""
select against_voucher_type, against_voucher,

View File

@@ -384,7 +384,11 @@ frappe.ui.form.on('Asset', {
set_values_from_purchase_doc: function(frm, doctype, purchase_doc) {
frm.set_value('company', purchase_doc.company);
frm.set_value('purchase_date', purchase_doc.posting_date);
if (purchase_doc.bill_date) {
frm.set_value('purchase_date', purchase_doc.bill_date);
} else {
frm.set_value('purchase_date', purchase_doc.posting_date);
}
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
doctype_field = frappe.scrub(doctype)

View File

@@ -819,7 +819,9 @@ class Asset(AccountsController):
def update_maintenance_status():
assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1})
assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
)
for asset in assets:
asset = frappe.get_doc("Asset", asset.name)

View File

@@ -7,7 +7,7 @@ import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries,
restore_asset,
@@ -238,6 +238,34 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_with_maintenance_required_status_after_sale(self):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-06-06",
purchase_date="2020-01-01",
expected_value_after_useful_life=10000,
total_number_of_depreciations=3,
frequency_of_depreciation=10,
maintenance_required=1,
depreciation_start_date="2020-12-31",
submit=1,
)
post_depreciation_entries(date="2021-01-01")
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.due_date = nowdate()
si.get("items")[0].rate = 25000
si.insert()
si.submit()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
update_maintenance_status()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
def test_expense_head(self):
pr = make_purchase_receipt(
item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location"
@@ -1353,6 +1381,7 @@ def create_asset(**args):
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location",

View File

@@ -43,6 +43,11 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
// On cancel and amending a purchase order with advance payment, reset advance paid amount
if (frm.is_new()) {
frm.set_value("advance_paid", 0)
}
},
apply_tds: function(frm) {

View File

@@ -439,7 +439,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1170,7 +1169,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-04-26 12:16:38.694276",
"modified": "2022-09-16 17:45:04.954055",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.party import get_party_account_currency
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
super(PurchaseOrder, self).on_cancel()
if self.is_against_so():
@@ -532,6 +533,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.set_advances()
target.set_payment_schedule()
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt)

View File

@@ -152,6 +152,7 @@ class AccountsController(TransactionBase):
self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate()
if self.meta.get_field("currency"):
@@ -395,6 +396,20 @@ class AccountsController(TransactionBase):
alert=1,
)
def disable_tax_included_prices_for_internal_transfer(self):
if self.is_internal_transfer():
tax_updated = False
for tax in self.get("taxes"):
if tax.get("included_in_print_rate"):
tax.included_in_print_rate = 0
tax_updated = True
if tax_updated:
frappe.msgprint(
_("Disabled tax included prices since this {} is an internal transfer").format(self.doctype),
alert=1,
)
def validate_due_date(self):
if self.get("is_pos"):
return

View File

@@ -193,16 +193,16 @@ class BuyingController(StockController, Subcontracting):
if self.meta.get_field("base_in_words"):
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
amount = self.base_rounded_total
amount = abs(self.base_rounded_total)
else:
amount = self.base_grand_total
amount = abs(self.base_grand_total)
self.base_in_words = money_in_words(amount, self.company_currency)
if self.meta.get_field("in_words"):
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
amount = self.rounded_total
amount = abs(self.rounded_total)
else:
amount = self.grand_total
amount = abs(self.grand_total)
self.in_words = money_in_words(amount, self.currency)
@@ -303,7 +303,11 @@ class BuyingController(StockController, Subcontracting):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),
d.precision("rate"),
)
if self.is_internal_transfer():
if rate != d.rate:

View File

@@ -439,11 +439,17 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if d.doctype == "Packed Item":
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision("incoming_rate"))
incoming_rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("incoming_rate"),
)
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
d.rate = rate
frappe.msgprint(

View File

@@ -139,13 +139,15 @@ class StockController(AccountsController):
warehouse_with_no_account = []
precision = self.get_debit_field_precision()
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
sle_rounding_diff = 0.0
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
# from warehouse account
sle_rounding_diff += flt(sle.stock_value_difference)
self.check_expense_account(item_row)
# expense account/ target_warehouse / source_warehouse
@@ -188,6 +190,46 @@ class StockController(AccountsController):
elif sle.warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(sle.warehouse)
if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
warehouse_asset_account = ""
if self.get("is_internal_customer"):
warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"]
elif self.get("is_internal_supplier"):
warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
expense_account = frappe.db.get_value("Company", self.company, "default_expense_account")
gl_list.append(
self.get_gl_dict(
{
"account": expense_account,
"against": warehouse_asset_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get("project"),
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
"debit": sle_rounding_diff,
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
},
warehouse_account[sle.warehouse]["account_currency"],
item=item_row,
)
)
gl_list.append(
self.get_gl_dict(
{
"account": warehouse_asset_account,
"against": expense_account,
"cost_center": item_row.cost_center,
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
"credit": sle_rounding_diff,
"project": item_row.get("project") or self.get("project"),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
},
item=item_row,
)
)
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):

View File

@@ -890,24 +890,33 @@ class calculate_taxes_and_totals(object):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def set_total_amount_to_default_mop(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value(
"POS Payment Method",
{"parent": self.doc.pos_profile, "default": 1},
["mode_of_payment"],
as_dict=1,
)
if default_mode_of_payment:
self.doc.payments = []
self.doc.append(
"payments",
{
"mode_of_payment": default_mode_of_payment.mode_of_payment,
"amount": total_amount_to_pay,
"default": 1,
},
total_paid_amount = 0
for payment in self.doc.get("payments"):
total_paid_amount += (
payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount
)
pending_amount = total_amount_to_pay - total_paid_amount
if pending_amount > 0:
default_mode_of_payment = frappe.db.get_value(
"POS Payment Method",
{"parent": self.doc.pos_profile, "default": 1},
["mode_of_payment"],
as_dict=1,
)
if default_mode_of_payment:
self.doc.payments = []
self.doc.append(
"payments",
{
"mode_of_payment": default_mode_of_payment.mode_of_payment,
"amount": pending_amount,
"default": 1,
},
)
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:

View File

@@ -11,9 +11,12 @@ frappe.ui.form.on('Course Scheduling Tool', {
},
refresh(frm) {
frm.disable_save();
frm.trigger("render_days");
frm.page.set_primary_action(__('Schedule Course'), () => {
frm.call('schedule_course')
frappe.dom.freeze(__("Scheduling..."));
frm.call('schedule_course', { days: frm.days.get_checked_options() })
.then(r => {
frappe.dom.unfreeze();
if (!r.message) {
frappe.throw(__('There were errors creating Course Schedule'));
}
@@ -40,5 +43,60 @@ frappe.ui.form.on('Course Scheduling Tool', {
}
});
});
},
render_days: function(frm) {
const days_html = $('<div class="days-editor">').appendTo(
frm.fields_dict.days_html.wrapper
);
if (!frm.days) {
frm.days = frappe.ui.form.make_control({
parent: days_html,
df: {
fieldname: "days",
fieldtype: "MultiCheck",
select_all: true,
columns: 4,
options: [
{
label: __("Monday"),
value: "Monday",
checked: 0,
},
{
label: __("Tuesday"),
value: "Tuesday",
checked: 0,
},
{
label: __("Wednesday"),
value: "Wednesday",
checked: 0,
},
{
label: __("Thursday"),
value: "Thursday",
checked: 0,
},
{
label: __("Friday"),
value: "Friday",
checked: 0,
},
{
label: __("Saturday"),
value: "Saturday",
checked: 0,
},
{
label: __("Sunday"),
value: "Sunday",
checked: 0,
},
],
},
render_input: true,
});
}
}
});

View File

@@ -1,661 +1,171 @@
{
"allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-09-23 15:37:38.108475",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"allow_copy": 1,
"creation": "2015-09-23 15:37:38.108475",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"student_group",
"course",
"program",
"column_break_3",
"academic_year",
"academic_term",
"section_break_6",
"instructor",
"instructor_name",
"column_break_9",
"room",
"section_break_7",
"days_html",
"section_break_14",
"from_time",
"course_start_date",
"column_break_15",
"to_time",
"course_end_date",
"reschedule"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "student_group",
"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": "Student Group",
"length": 0,
"no_copy": 0,
"options": "Student Group",
"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
},
"fieldname": "student_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Student Group",
"options": "Student Group",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "course",
"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": "Course",
"length": 0,
"no_copy": 0,
"options": "Course",
"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
},
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "Course",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "program",
"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": "Program",
"length": 0,
"no_copy": 0,
"options": "Program",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "program",
"fieldtype": "Link",
"label": "Program",
"options": "Program",
"read_only": 1
},
{
"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": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "academic_year",
"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": "Academic Year",
"length": 0,
"no_copy": 0,
"options": "Academic Year",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "academic_year",
"fieldtype": "Link",
"label": "Academic Year",
"options": "Academic Year",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "academic_term",
"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": "Academic Term",
"length": 0,
"no_copy": 0,
"options": "Academic Term",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "academic_term",
"fieldtype": "Link",
"label": "Academic Term",
"options": "Academic Term",
"read_only": 1
},
{
"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": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "instructor",
"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": "Instructor",
"length": 0,
"no_copy": 0,
"options": "Instructor",
"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
},
"fieldname": "instructor",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Instructor",
"options": "Instructor",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "instructor.instructor_name",
"fieldname": "instructor_name",
"fieldtype": "Read Only",
"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": "Instructor Name",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "instructor_name",
"fieldtype": "Read Only",
"label": "Instructor Name",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"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_9",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "room",
"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": "Room",
"length": 0,
"no_copy": 0,
"options": "Room",
"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
},
"fieldname": "room",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Room",
"options": "Room",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"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_7",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "from_time",
"fieldtype": "Time",
"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": "From Time",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "from_time",
"fieldtype": "Time",
"label": "From Time",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "course_start_date",
"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": "Course Start Date",
"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
},
"fieldname": "course_start_date",
"fieldtype": "Date",
"label": "Course Start Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "day",
"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": "Day",
"length": 0,
"no_copy": 0,
"options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
"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
},
"default": "0",
"fieldname": "reschedule",
"fieldtype": "Check",
"label": "Reschedule"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reschedule",
"fieldtype": "Check",
"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": "Reschedule",
"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_15",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_15",
"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": "to_time",
"fieldtype": "Time",
"label": "To TIme",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_time",
"fieldtype": "Time",
"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": "To TIme",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "course_end_date",
"fieldtype": "Date",
"label": "Course End Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "course_end_date",
"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": "Course End Date",
"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,
"translatable": 0,
"unique": 0
"fieldname": "days_html",
"fieldtype": "HTML",
"label": "Days HTML"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
}
],
"has_web_view": 0,
"hide_heading": 1,
"hide_toolbar": 1,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-05-16 22:43:29.363798",
"modified_by": "Administrator",
"module": "Education",
"name": "Course Scheduling Tool",
"name_case": "",
"owner": "Administrator",
],
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2022-10-01 17:08:07.180557",
"modified_by": "Administrator",
"module": "Education",
"name": "Course Scheduling Tool",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Academics User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"create": 1,
"read": 1,
"role": "Academics User",
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"restrict_to_domain": "Education",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -14,7 +14,7 @@ from erpnext.education.utils import OverlapError
class CourseSchedulingTool(Document):
@frappe.whitelist()
def schedule_course(self):
def schedule_course(self, days):
"""Creates course schedules as per specified parameters"""
course_schedules = []
@@ -22,7 +22,7 @@ class CourseSchedulingTool(Document):
rescheduled = []
reschedule_errors = []
self.validate_mandatory()
self.validate_mandatory(days)
self.validate_date()
self.instructor_name = frappe.db.get_value("Instructor", self.instructor, "instructor_name")
@@ -34,24 +34,22 @@ class CourseSchedulingTool(Document):
self.course = course
if self.reschedule:
rescheduled, reschedule_errors = self.delete_course_schedule(rescheduled, reschedule_errors)
rescheduled, reschedule_errors = self.delete_course_schedule(
rescheduled, reschedule_errors, days
)
date = self.course_start_date
while date < self.course_end_date:
if self.day == calendar.day_name[getdate(date).weekday()]:
if calendar.day_name[getdate(date).weekday()] in days:
course_schedule = self.make_course_schedule(date)
try:
print("pass")
course_schedule.save()
except OverlapError:
print("fail")
course_schedules_errors.append(date)
else:
course_schedules.append(course_schedule)
date = add_days(date, 7)
else:
date = add_days(date, 1)
date = add_days(date, 1)
return dict(
course_schedules=course_schedules,
@@ -60,8 +58,10 @@ class CourseSchedulingTool(Document):
reschedule_errors=reschedule_errors,
)
def validate_mandatory(self):
def validate_mandatory(self, days):
"""Validates all mandatory fields"""
if not days:
frappe.throw(_("Please select at least one day to schedule the course."))
fields = [
"course",
@@ -71,7 +71,6 @@ class CourseSchedulingTool(Document):
"to_time",
"course_start_date",
"course_end_date",
"day",
]
for d in fields:
if not self.get(d):
@@ -82,9 +81,8 @@ class CourseSchedulingTool(Document):
if self.course_start_date > self.course_end_date:
frappe.throw(_("Course Start Date cannot be greater than Course End Date."))
def delete_course_schedule(self, rescheduled, reschedule_errors):
def delete_course_schedule(self, rescheduled, reschedule_errors, days):
"""Delete all course schedule within the Date range and specified filters"""
schedules = frappe.get_list(
"Course Schedule",
fields=["name", "schedule_date"],
@@ -98,7 +96,7 @@ class CourseSchedulingTool(Document):
for d in schedules:
try:
if self.day == calendar.day_name[getdate(d.schedule_date).weekday()]:
if calendar.day_name[getdate(d.schedule_date).weekday()] in days:
frappe.delete_doc("Course Schedule", d.name)
rescheduled.append(d.name)
except Exception:
@@ -108,7 +106,6 @@ class CourseSchedulingTool(Document):
def make_course_schedule(self, date):
"""Makes a new Course Schedule.
:param date: Date on which Course Schedule will be created."""
course_schedule = frappe.new_doc("Course Schedule")
course_schedule.student_group = self.student_group
course_schedule.course = self.course

View File

@@ -334,8 +334,6 @@ has_website_permission = {
"Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission",
}
dump_report_map = "erpnext.startup.report_data_map.data_map"
before_tests = "erpnext.setup.utils.before_tests"
standard_queries = {
@@ -478,7 +476,6 @@ scheduler_events = {
],
"hourly": [
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
@@ -487,6 +484,7 @@ scheduler_events = {
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
],

View File

@@ -253,10 +253,12 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
start_day = 1
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
if joining_date and joining_date.month == month_map[month]:
if joining_date and joining_date.year == today.year and joining_date.month == month_map[month]:
start_day = joining_date.day
if relieving_date and relieving_date.month == month_map[month]:
if (
relieving_date and relieving_date.year == today.year and relieving_date.month == month_map[month]
):
end_day = relieving_date.day + 1
dates_of_month = [

View File

@@ -13,6 +13,8 @@ frappe.listview_settings['Attendance'] = {
onload: function(list_view) {
let me = this;
const months = moment.months();
const curMonth = moment().format("MMMM");
months.splice(months.indexOf(curMonth) + 1);
list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),

View File

@@ -19,7 +19,7 @@ from erpnext.hr.doctype.attendance.attendance import (
mark_attendance,
)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
from erpnext.hr.tests.test_utils import get_first_sunday
test_records = frappe.get_test_records("Attendance")

View File

@@ -33,6 +33,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
@@ -1105,23 +1106,6 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit()
def get_first_sunday(holiday_list, for_date=None):
date = for_date or getdate()
month_start_date = get_first_day(date)
month_end_date = get_last_day(date)
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = %s
and holiday_date between %s and %s
order by holiday_date
""",
(holiday_list, month_start_date, month_end_date),
)[0][0]
return first_sunday
def make_policy_assignment(employee, leave_type, leave_period):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
frappe.get_doc(

View File

@@ -9,13 +9,11 @@ from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_st
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_first_sunday,
make_allocation_record,
)
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,

View File

@@ -22,9 +22,9 @@ def execute(filters=None):
def get_columns(leave_types):
columns = [
_("Employee") + ":Link.Employee:150",
_("Employee") + ":Link/Employee:150",
_("Employee Name") + "::200",
_("Department") + "::150",
_("Department") + ":Link/Department:150",
]
for leave_type in leave_types:

View File

@@ -9,12 +9,10 @@ from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_first_sunday,
make_allocation_record,
)
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,

View File

@@ -181,7 +181,6 @@ def add_data(
total_l += 1
elif status == "Half Day":
total_p += 0.5
total_a += 0.5
total_l += 0.5
elif not status:
total_um += 1

View File

@@ -0,0 +1,19 @@
import frappe
from frappe.utils import get_first_day, get_last_day, getdate
def get_first_sunday(holiday_list="Salary Slip Test Holiday List", for_date=None):
date = for_date or getdate()
month_start_date = get_first_day(date)
month_end_date = get_last_day(date)
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = %s
and holiday_date between %s and %s
order by holiday_date
""",
(holiday_list, month_start_date, month_end_date),
)[0][0]
return first_sunday

View File

@@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', {
},
refresh: function (frm) {
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
}
if (frm.doc.docstatus == 1) {
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Request Loan Closure'), function() {
@@ -103,6 +107,14 @@ frappe.ui.form.on('Loan', {
frm.trigger("toggle_fields");
},
repayment_schedule_type: function(frm) {
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
} else {
frm.set_df_property("repayment_start_date", "label", "Repayment Start Date");
}
},
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", frm.doc.is_term_loan);

View File

@@ -17,8 +17,10 @@
"posting_date",
"status",
"repay_from_salary",
"manually_update_paid_amount_in_salary_slip",
"section_break_8",
"loan_type",
"repayment_schedule_type",
"loan_amount",
"rate_of_interest",
"is_secured_loan",
@@ -51,10 +53,10 @@
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
"is_npa",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
"is_npa",
"amended_from"
],
"fields": [
@@ -166,7 +168,8 @@
"depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
"label": "Repayment Start Date"
"label": "Repayment Start Date",
"mandatory_depends_on": "is_term_loan"
},
{
"fieldname": "column_break_11",
@@ -410,16 +413,31 @@
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
},
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "repay_from_salary",
"fieldname": "manually_update_paid_amount_in_salary_slip",
"fieldtype": "Check",
"label": "Manually Update Paid Amount in Salary Slip"
},
{
"depends_on": "is_term_loan",
"fetch_from": "loan_type.repayment_schedule_type",
"fieldname": "repayment_schedule_type",
"fieldtype": "Data",
"label": "Repayment Schedule Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-06-30 12:04:13.728880",
"modified": "2022-11-01 10:36:47.902903",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -445,6 +463,5 @@
"search_fields": "posting_date",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -7,7 +7,16 @@ import math
import frappe
from frappe import _
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
from frappe.utils import (
add_days,
add_months,
date_diff,
flt,
get_last_day,
getdate,
now_datetime,
nowdate,
)
from six import string_types
import erpnext
@@ -115,30 +124,81 @@ class Loan(AccountsController):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
schedule_type_details = frappe.db.get_value(
"Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1
)
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while balance_amount > 0:
interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
self.append(
"repayment_schedule",
{
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount,
},
while balance_amount > 0:
interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts(
payment_date,
balance_amount,
schedule_type_details.repayment_schedule_type,
schedule_type_details.repayment_date_on,
)
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months":
next_payment_date = get_last_day(payment_date)
if schedule_type_details.repayment_date_on == "Start of the next month":
next_payment_date = add_days(next_payment_date, 1)
payment_date = next_payment_date
self.add_repayment_schedule_row(
payment_date, principal_amount, interest_amount, total_payment, balance_amount
)
if (
schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date"
or schedule_type_details.repayment_date_on == "End of the current month"
):
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on):
if schedule_type == "Monthly as per repayment start date":
days = 1
months = 12
else:
expected_payment_date = get_last_day(payment_date)
if repayment_date_on == "Start of the next month":
expected_payment_date = add_days(expected_payment_date, 1)
if expected_payment_date == payment_date:
# using 30 days for calculating interest for all full months
days = 30
months = 365
else:
days = date_diff(get_last_day(payment_date), payment_date)
months = 365
interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
return interest_amount, principal_amount, balance_amount, total_payment
def add_repayment_schedule_row(
self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount
):
self.append(
"repayment_schedule",
{
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_loan_amount,
},
)
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":

View File

@@ -4,7 +4,16 @@
import unittest
import frappe
from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate
from frappe.utils import (
add_days,
add_months,
add_to_date,
date_diff,
flt,
format_date,
get_datetime,
nowdate,
)
from erpnext.loan_management.doctype.loan.loan import (
make_loan_write_off,
@@ -50,6 +59,51 @@ class TestLoan(unittest.TestCase):
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
repayment_schedule_type="Monthly as per repayment start date",
)
create_loan_type(
"Term Loan Type 1",
12000,
7.5,
is_term_loan=1,
mode_of_payment="Cash",
disbursement_account="Disbursement Account - _TC",
payment_account="Payment Account - _TC",
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
repayment_schedule_type="Monthly as per repayment start date",
)
create_loan_type(
"Term Loan Type 2",
12000,
7.5,
is_term_loan=1,
mode_of_payment="Cash",
disbursement_account="Disbursement Account - _TC",
payment_account="Payment Account - _TC",
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
repayment_schedule_type="Pro-rated calendar months",
repayment_date_on="Start of the next month",
)
create_loan_type(
"Term Loan Type 3",
12000,
7.5,
is_term_loan=1,
mode_of_payment="Cash",
disbursement_account="Disbursement Account - _TC",
payment_account="Payment Account - _TC",
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
repayment_schedule_type="Pro-rated calendar months",
repayment_date_on="End of the current month",
)
create_loan_type(
@@ -65,6 +119,7 @@ class TestLoan(unittest.TestCase):
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
repayment_schedule_type="Monthly as per repayment start date",
)
create_loan_type(
@@ -912,6 +967,69 @@ class TestLoan(unittest.TestCase):
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0)
def test_term_loan_schedule_types(self):
loan = create_loan(
self.applicant1,
"Term Loan Type 1",
12000,
"Repay Over Number of Periods",
12,
repayment_start_date="2022-10-17",
)
# Check for first, second and last installment date
self.assertEqual(
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023"
)
loan.loan_type = "Term Loan Type 2"
loan.save()
# Check for first, second and last installment date
self.assertEqual(
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023"
)
loan.loan_type = "Term Loan Type 3"
loan.save()
# Check for first, second and last installment date
self.assertEqual(
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 1042
loan.save()
self.assertEqual(
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
)
self.assertEqual(
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
)
def create_loan_scenario_for_penalty(doc):
pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
@@ -1043,6 +1161,8 @@ def create_loan_type(
penalty_income_account=None,
repayment_method=None,
repayment_periods=None,
repayment_schedule_type=None,
repayment_date_on=None,
):
if not frappe.db.exists("Loan Type", loan_name):
@@ -1052,6 +1172,7 @@ def create_loan_type(
"company": "_Test Company",
"loan_name": loan_name,
"is_term_loan": is_term_loan,
"repayment_schedule_type": "Monthly as per repayment start date",
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest,
"penalty_interest_rate": penalty_interest_rate,
@@ -1066,8 +1187,14 @@ def create_loan_type(
"repayment_periods": repayment_periods,
"write_off_amount": 100,
}
).insert()
)
if loan_type.is_term_loan:
loan_type.repayment_schedule_type = repayment_schedule_type
if loan_type.repayment_schedule_type != "Monthly as per repayment start date":
loan_type.repayment_date_on = repayment_date_on
loan_type.insert()
loan_type.submit()

View File

@@ -520,6 +520,8 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
if not posting_date:
posting_date = getdate()
precision = cint(frappe.db.get_default("currency_precision")) or 2
unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
@@ -540,6 +542,13 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
as_dict=1,
)
# Skip entries with zero interest amount & payable principal amount
unpaid_accrued_entries = [
d
for d in unpaid_accrued_entries
if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0
]
return unpaid_accrued_entries

View File

@@ -16,6 +16,8 @@
"company",
"is_term_loan",
"disabled",
"repayment_schedule_type",
"repayment_date_on",
"description",
"account_details_section",
"mode_of_payment",
@@ -157,12 +159,30 @@
"label": "Disbursement Account",
"options": "Account",
"reqd": 1
},
{
"depends_on": "is_term_loan",
"description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)",
"fieldname": "repayment_schedule_type",
"fieldtype": "Select",
"label": "Repayment Schedule Type",
"mandatory_depends_on": "is_term_loan",
"options": "\nMonthly as per repayment start date\nPro-rated calendar months"
},
{
"depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
"description": "Select whether the repayment date should be the end of the current month or start of the upcoming month",
"fieldname": "repayment_date_on",
"fieldtype": "Select",
"label": "Repayment Date On",
"mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
"options": "\nStart of the next month\nEnd of the current month"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-25 16:23:57.009349",
"modified": "2022-11-01 17:43:03.954201",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",

View File

@@ -384,6 +384,7 @@ class BOM(WebsiteGenerator):
if self.docstatus == 2:
return
self.flags.cost_updated = False
existing_bom_cost = self.total_cost
if self.docstatus == 1:
@@ -406,7 +407,11 @@ class BOM(WebsiteGenerator):
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom:
frappe.msgprint(_("Cost Updated"), alert=True)
msg = "Cost Updated"
if not self.flags.cost_updated:
msg = "No changes in cost found"
frappe.msgprint(_(msg), alert=True)
def update_parent_cost(self):
if self.total_cost:
@@ -592,11 +597,16 @@ class BOM(WebsiteGenerator):
# not via doc event, table is not regenerated and needs updation
self.calculate_exploded_cost()
old_cost = self.total_cost
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = (
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
)
if self.total_cost != old_cost:
self.flags.cost_updated = True
def calculate_op_cost(self, update_hour_rate=False):
"""Update workstation rate and calculates totals"""
self.operating_cost = 0

View File

@@ -583,6 +583,28 @@ class TestBOM(FrappeTestCase):
bom.submit()
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
def test_bom_cost_update_flag(self):
rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
).name
fg_item = make_item(properties={"is_stock_item": 1}).name
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
bom = make_bom(item=fg_item, raw_materials=[rm_item])
create_stock_reconciliation(
item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600
)
bom.load_from_db()
bom.update_cost()
self.assertTrue(bom.flags.cost_updated)
bom.load_from_db()
bom.update_cost()
self.assertFalse(bom.flags.cost_updated)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -133,7 +133,7 @@ class JobCard(Document):
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
)
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
order by jctl.to_time desc limit 1""".format(
order by jctl.to_time desc""".format(
extra_cond, validate_overlap_for
),
{

View File

@@ -133,6 +133,45 @@ class TestJobCard(FrappeTestCase):
)
self.assertRaises(OverlapError, jc2.save)
def test_job_card_overlap_with_capacity(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
workstation = make_workstation(workstation_name=random_string(5)).name
frappe.db.set_value("Workstation", workstation, "production_capacity", 1)
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
jc1.workstation = workstation
jc1.append(
"time_logs",
{"from_time": "2021-01-01 00:00:00", "to_time": "2021-01-01 08:00:00", "completed_qty": 1},
)
jc1.save()
jc2.workstation = workstation
# add a new entry in same time slice
jc2.append(
"time_logs",
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
)
self.assertRaises(OverlapError, jc2.save)
frappe.db.set_value("Workstation", workstation, "production_capacity", 2)
jc2.load_from_db()
jc2.workstation = workstation
# add a new entry in same time slice
jc2.append(
"time_logs",
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
)
jc2.save()
self.assertTrue(jc2.name)
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
self.transfer_material_against = "Job Card"

View File

@@ -8,6 +8,7 @@ import json
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
add_days,
ceil,
@@ -20,11 +21,13 @@ from frappe.utils import (
nowdate,
)
from frappe.utils.csvutils import build_csv_response
from pypika.terms import ExistsCriterion
from six import iteritems
from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -100,39 +103,46 @@ class ProductionPlan(Document):
@frappe.whitelist()
def get_pending_material_requests(self):
"""Pull Material Requests that are pending based on criteria selected"""
mr_filter = item_filter = ""
bom = frappe.qb.DocType("BOM")
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
pending_mr_query = (
frappe.qb.from_(mr)
.from_(mr_item)
.select(mr.name, mr.transaction_date)
.distinct()
.where(
(mr_item.parent == mr.name)
& (mr.material_request_type == "Manufacture")
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == self.company)
& (mr_item.qty > IfNull(mr_item.ordered_qty, 0))
& (
ExistsCriterion(
frappe.qb.from_(bom)
.select(bom.name)
.where((bom.item == mr_item.item_code) & (bom.is_active == 1))
)
)
)
)
if self.from_date:
mr_filter += " and mr.transaction_date >= %(from_date)s"
pending_mr_query = pending_mr_query.where(mr.transaction_date >= self.from_date)
if self.to_date:
mr_filter += " and mr.transaction_date <= %(to_date)s"
pending_mr_query = pending_mr_query.where(mr.transaction_date <= self.to_date)
if self.warehouse:
mr_filter += " and mr_item.warehouse = %(warehouse)s"
pending_mr_query = pending_mr_query.where(mr_item.warehouse == self.warehouse)
if self.item_code:
item_filter += " and mr_item.item_code = %(item)s"
pending_mr_query = pending_mr_query.where(mr_item.item_code == self.item_code)
pending_mr = frappe.db.sql(
"""
select distinct mr.name, mr.transaction_date
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr_item.parent = mr.name
and mr.material_request_type = "Manufacture"
and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1))
""".format(
mr_filter, item_filter
),
{
"from_date": self.from_date,
"to_date": self.to_date,
"warehouse": self.warehouse,
"item": self.item_code,
"company": self.company,
},
as_dict=1,
)
pending_mr = pending_mr_query.run(as_dict=True)
self.add_mr_in_table(pending_mr)
@@ -160,16 +170,17 @@ class ProductionPlan(Document):
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
return so_mr_list
def get_bom_item(self):
def get_bom_item_condition(self):
"""Check if Item or if its Template has a BOM."""
bom_item = None
bom_item_condition = None
has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1})
if not has_bom:
bom = frappe.qb.DocType("BOM")
template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"])
bom_item = (
"bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
)
return bom_item
bom_item_condition = bom.item == template_item or None
return bom_item_condition
def get_so_items(self):
# Check for empty table or empty rows
@@ -178,46 +189,75 @@ class ProductionPlan(Document):
so_list = self.get_so_mr_list("sales_order", "sales_orders")
item_condition = ""
bom_item = "bom.item = so_item.item_code"
if self.item_code and frappe.db.exists("Item", self.item_code):
bom_item = self.get_bom_item() or bom_item
item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
bom = frappe.qb.DocType("BOM")
so_item = frappe.qb.DocType("Sales Order Item")
items = frappe.db.sql(
"""
select
distinct parent, item_code, warehouse,
(qty - work_order_qty) * conversion_factor as pending_qty,
description, name
from
`tabSales Order Item` so_item
where
parent in (%s) and docstatus = 1 and qty > work_order_qty
and exists (select name from `tabBOM` bom where %s
and bom.is_active = 1) %s"""
% (", ".join(["%s"] * len(so_list)), bom_item, item_condition),
tuple(so_list),
as_dict=1,
items_subquery = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1)
items_query = (
frappe.qb.from_(so_item)
.select(
so_item.parent,
so_item.item_code,
so_item.warehouse,
(
(so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
).as_("pending_qty"),
so_item.description,
so_item.name,
)
.distinct()
.where(
(so_item.parent.isin(so_list))
& (so_item.docstatus == 1)
& (so_item.qty > so_item.work_order_qty)
)
)
if self.item_code and frappe.db.exists("Item", self.item_code):
items_query = items_query.where(so_item.item_code == self.item_code)
items_subquery = items_subquery.where(
self.get_bom_item_condition() or bom.item == so_item.item_code
)
items_query = items_query.where(ExistsCriterion(items_subquery))
items = items_query.run(as_dict=True)
pi = frappe.qb.DocType("Packed Item")
packed_items_query = (
frappe.qb.from_(so_item)
.from_(pi)
.select(
pi.parent,
pi.item_code,
pi.warehouse.as_("warehouse"),
(((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"),
pi.parent_item,
pi.description,
so_item.name,
)
.distinct()
.where(
(so_item.parent == pi.parent)
& (so_item.docstatus == 1)
& (pi.parent_item == so_item.item_code)
& (so_item.parent.isin(so_list))
& (so_item.qty > so_item.work_order_qty)
& (
ExistsCriterion(
frappe.qb.from_(bom)
.select(bom.name)
.where((bom.item == pi.item_code) & (bom.is_active == 1))
)
)
)
)
if self.item_code:
item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
packed_items_query = packed_items_query.where(so_item.item_code == self.item_code)
packed_items = frappe.db.sql(
"""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse,
(((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty)
as pending_qty, pi.parent_item, pi.description, so_item.name
from `tabSales Order Item` so_item, `tabPacked Item` pi
where so_item.parent = pi.parent and so_item.docstatus = 1
and pi.parent_item = so_item.item_code
and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1) %s"""
% (", ".join(["%s"] * len(so_list)), item_condition),
tuple(so_list),
as_dict=1,
)
packed_items = packed_items_query.run(as_dict=True)
self.add_items(items + packed_items)
self.calculate_total_planned_qty()
@@ -233,22 +273,39 @@ class ProductionPlan(Document):
mr_list = self.get_so_mr_list("material_request", "material_requests")
item_condition = ""
if self.item_code:
item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code))
bom = frappe.qb.DocType("BOM")
mr_item = frappe.qb.DocType("Material Request Item")
items = frappe.db.sql(
"""select distinct parent, name, item_code, warehouse, description,
(qty - ordered_qty) * conversion_factor as pending_qty
from `tabMaterial Request Item` mr_item
where parent in (%s) and docstatus = 1 and qty > ordered_qty
and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1) %s"""
% (", ".join(["%s"] * len(mr_list)), item_condition),
tuple(mr_list),
as_dict=1,
items_query = (
frappe.qb.from_(mr_item)
.select(
mr_item.parent,
mr_item.name,
mr_item.item_code,
mr_item.warehouse,
mr_item.description,
((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"),
)
.distinct()
.where(
(mr_item.parent.isin(mr_list))
& (mr_item.docstatus == 1)
& (mr_item.qty > mr_item.ordered_qty)
& (
ExistsCriterion(
frappe.qb.from_(bom)
.select(bom.name)
.where((bom.item == mr_item.item_code) & (bom.is_active == 1))
)
)
)
)
if self.item_code:
items_query = items_query.where(mr_item.item_code == self.item_code)
items = items_query.run(as_dict=True)
self.add_items(items)
self.calculate_total_planned_qty()
@@ -595,13 +652,23 @@ class ProductionPlan(Document):
else:
material_request = material_request_map[key]
conversion_factor = 1.0
if (
material_request_type == "Purchase"
and item_doc.purchase_uom
and item_doc.purchase_uom != item_doc.stock_uom
):
conversion_factor = (
get_conversion_factor(item_doc.name, item_doc.purchase_uom).get("conversion_factor") or 1.0
)
# add item
material_request.append(
"items",
{
"item_code": item.item_code,
"from_warehouse": item.from_warehouse,
"qty": item.quantity,
"qty": item.quantity / conversion_factor,
"schedule_date": schedule_date,
"warehouse": item.warehouse,
"sales_order": item.sales_order,
@@ -754,29 +821,46 @@ def download_raw_materials(doc, warehouses=None):
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
for d in frappe.db.sql(
"""select bei.item_code, item.default_bom as bom,
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
item.purchase_uom, item_uom.conversion_factor, item.safety_stock
from
`tabBOM Explosion Item` bei
JOIN `tabBOM` bom ON bom.name = bei.parent
JOIN `tabItem` item ON item.name = bei.item_code
LEFT JOIN `tabItem Default` item_default
ON item_default.parent = item.name and item_default.company=%s
LEFT JOIN `tabUOM Conversion Detail` item_uom
ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
where
bei.docstatus < 2
and bom.name=%s and item.is_stock_item in (1, {0})
group by bei.item_code, bei.stock_uom""".format(
0 if include_non_stock_items else 1
),
(planned_qty, company, bom_no),
as_dict=1,
):
bei = frappe.qb.DocType("BOM Explosion Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
item_uom = frappe.qb.DocType("UOM Conversion Detail")
data = (
frappe.qb.from_(bei)
.join(bom)
.on(bom.name == bei.parent)
.join(item)
.on(item.name == bei.item_code)
.left_join(item_default)
.on((item_default.parent == item.name) & (item_default.company == company))
.left_join(item_uom)
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
.select(
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name,
item.name.as_("item_code"),
bei.description,
bei.stock_uom,
item.min_order_qty,
bei.source_warehouse,
item.default_material_request_type,
item.min_order_qty,
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
)
.where(
(bei.docstatus < 2)
& (bom.name == bom_no)
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
)
.groupby(bei.item_code, bei.stock_uom)
).run(as_dict=True)
for d in data:
if not d.conversion_factor and d.purchase_uom:
d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom)
item_details.setdefault(d.get("item_code"), d)
@@ -801,33 +885,47 @@ def get_subitems(
parent_qty,
planned_qty=1,
):
items = frappe.db.sql(
"""
SELECT
bom_item.item_code, default_material_request_type, item.item_name,
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
item.default_bom as default_bom, bom_item.description as description,
bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock,
item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor
FROM
`tabBOM Item` bom_item
JOIN `tabBOM` bom ON bom.name = bom_item.parent
JOIN tabItem item ON bom_item.item_code = item.name
LEFT JOIN `tabItem Default` item_default
ON item.name = item_default.parent and item_default.company = %(company)s
LEFT JOIN `tabUOM Conversion Detail` item_uom
ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
where
bom.name = %(bom)s
and bom_item.docstatus < 2
and item.is_stock_item in (1, {0})
group by bom_item.item_code""".format(
0 if include_non_stock_items else 1
),
{"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company},
as_dict=1,
)
bom_item = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
item_uom = frappe.qb.DocType("UOM Conversion Detail")
items = (
frappe.qb.from_(bom_item)
.join(bom)
.on(bom.name == bom_item.parent)
.join(item)
.on(bom_item.item_code == item.name)
.left_join(item_default)
.on((item.name == item_default.parent) & (item_default.company == company))
.left_join(item_uom)
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
.select(
bom_item.item_code,
item.default_material_request_type,
item.item_name,
IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_(
"qty"
),
item.is_sub_contracted_item.as_("is_sub_contracted"),
bom_item.source_warehouse,
item.default_bom.as_("default_bom"),
bom_item.description.as_("description"),
bom_item.stock_uom.as_("stock_uom"),
item.min_order_qty.as_("min_order_qty"),
item.safety_stock.as_("safety_stock"),
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
)
.where(
(bom.name == bom_no)
& (bom_item.docstatus < 2)
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
)
.groupby(bom_item.item_code)
).run(as_dict=True)
for d in items:
if not data.get("include_exploded_items") or not d.default_bom:
@@ -915,48 +1013,69 @@ def get_material_request_items(
def get_sales_orders(self):
so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
bom = frappe.qb.DocType("BOM")
pi = frappe.qb.DocType("Packed Item")
so = frappe.qb.DocType("Sales Order")
so_item = frappe.qb.DocType("Sales Order Item")
open_so_subquery1 = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1)
open_so_subquery2 = (
frappe.qb.from_(pi)
.select(pi.name)
.where(
(pi.parent == so.name)
& (pi.parent_item == so_item.item_code)
& (
ExistsCriterion(
frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1))
)
)
)
)
open_so_query = (
frappe.qb.from_(so)
.from_(so_item)
.select(so.name, so.transaction_date, so.customer, so.base_grand_total)
.distinct()
.where(
(so_item.parent == so.name)
& (so.docstatus == 1)
& (so.status.notin(["Stopped", "Closed"]))
& (so.company == self.company)
& (so_item.qty > so_item.work_order_qty)
)
)
date_field_mapper = {
"from_date": (">=", "so.transaction_date"),
"to_date": ("<=", "so.transaction_date"),
"from_delivery_date": (">=", "so_item.delivery_date"),
"to_delivery_date": ("<=", "so_item.delivery_date"),
"from_date": self.from_date >= so.transaction_date,
"to_date": self.to_date <= so.transaction_date,
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
}
for field, value in date_field_mapper.items():
if self.get(field):
so_filter += f" and {value[1]} {value[0]} %({field})s"
open_so_query = open_so_query.where(value)
for field in ["customer", "project", "sales_order_status"]:
for field in ("customer", "project", "sales_order_status"):
if self.get(field):
so_field = "status" if field == "sales_order_status" else field
so_filter += f" and so.{so_field} = %({field})s"
open_so_query = open_so_query.where(so[so_field] == self.get(field))
if self.item_code and frappe.db.exists("Item", self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item_code)s"
open_so_query = open_so_query.where(so_item.item_code == self.item_code)
open_so_subquery1 = open_so_subquery1.where(
self.get_bom_item_condition() or bom.item == so_item.item_code
)
open_so = frappe.db.sql(
f"""
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
and (exists (select name from `tabBOM` bom where {bom_item}
and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
""",
self.as_dict(),
as_dict=1,
open_so_query = open_so_query.where(
(ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2))
)
open_so = open_so_query.run(as_dict=True)
return open_so
@@ -965,37 +1084,34 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
if isinstance(row, str):
row = frappe._dict(json.loads(row))
company = frappe.db.escape(company)
conditions, warehouse = "", ""
bin = frappe.qb.DocType("Bin")
wh = frappe.qb.DocType("Warehouse")
subquery = frappe.qb.from_(wh).select(wh.name).where(wh.company == company)
conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(
company
)
if not all_warehouse:
warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse")
if warehouse:
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
conditions = """ and warehouse in (select name from `tabWarehouse`
where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2})
""".format(
lft, rgt, company
)
subquery = subquery.where((wh.lft >= lft) & (wh.rgt <= rgt) & (wh.name == bin.warehouse))
return frappe.db.sql(
""" select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
ifnull(sum(planned_qty),0) as planned_qty
from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse
""".format(
conditions=conditions
),
{"item_code": row["item_code"]},
as_dict=1,
query = (
frappe.qb.from_(bin)
.select(
bin.warehouse,
IfNull(Sum(bin.projected_qty), 0).as_("projected_qty"),
IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
IfNull(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
IfNull(Sum(bin.reserved_qty_for_production), 0).as_("reserved_qty_for_production"),
IfNull(Sum(bin.planned_qty), 0).as_("planned_qty"),
)
.where((bin.item_code == row["item_code"]) & (bin.warehouse.isin(subquery)))
.groupby(bin.item_code, bin.warehouse)
)
return query.run(as_dict=True)
@frappe.whitelist()
def get_so_details(sales_order):

View File

@@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
)
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -538,15 +539,21 @@ class TestProductionPlan(FrappeTestCase):
"""
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
make_stock_entry(item_code="_Test Item", target="Work In Progress - _TC", qty=2, basic_rate=100)
make_stock_entry(
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
)
make_stock_entry(
item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100
item_code="_Test Item Home Desktop 100", target="Work In Progress - _TC", qty=4, basic_rate=100
)
item = "Test Production Item 1"
so = make_sales_order(item_code=item, qty=1)
item = "_Test FG Item"
make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1)
so = make_sales_order(item_code=item, qty=2)
dn = make_delivery_note(so.name)
dn.items[0].qty = 1
dn.save()
dn.submit()
pln = create_production_plan(
company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True
@@ -725,6 +732,35 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(pln.status, "Completed")
self.assertEqual(pln.po_items[0].produced_qty, 5)
def test_material_request_item_for_purchase_uom(self):
from erpnext.stock.doctype.item.test_item import make_item
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
bom_item = make_item(
properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
).name
if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
doc = frappe.get_doc("Item", bom_item)
doc.append("uoms", {"uom": "Nos", "conversion_factor": 10})
doc.save()
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(
item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
)
pln.make_material_request()
for row in frappe.get_all(
"Material Request Item",
filters={"production_plan": pln.name},
fields=["item_code", "uom", "qty"],
):
self.assertEqual(row.item_code, bom_item)
self.assertEqual(row.uom, "Nos")
self.assertEqual(row.qty, 1)
def create_production_plan(**args):
"""

View File

@@ -555,13 +555,10 @@ erpnext.work_order = {
}
}
if(!frm.doc.skip_transfer){
if (frm.doc.status != 'Stopped') {
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
&& frm.doc.status != 'Stopped') {
frm.has_finish_btn = true;
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) {
// Only show "Material Consumption" when required_qty > consumed_qty
var counter = 0;
var tbl = frm.doc.required_items || [];
@@ -580,26 +577,47 @@ erpnext.work_order = {
consumption_btn.addClass('btn-primary');
}
}
}
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
if(!frm.doc.skip_transfer){
if (flt(doc.material_transferred_for_manufacturing) > 0) {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
frm.has_finish_btn = true;
if(doc.material_transferred_for_manufacturing>=doc.qty) {
// all materials transferred for manufacturing, make this primary
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
if(doc.material_transferred_for_manufacturing>=doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
} else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
}
});
}
}
} else {
if ((flt(doc.produced_qty) < flt(doc.qty))) {
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
finish_btn.addClass('btn-primary');
}
}
} else {
if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
finish_btn.addClass('btn-primary');
}
}
}
},
calculate_cost: function(doc) {
if (doc.operations){

View File

@@ -11,17 +11,24 @@ frappe.query_reports["BOM Stock Calculated"] = {
"options": "BOM",
"reqd": 1
},
{
"fieldname": "qty_to_make",
"label": __("Quantity to Make"),
"fieldtype": "Int",
"default": "1"
},
{
{
"fieldname": "warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse",
},
{
"fieldname": "qty_to_make",
"label": __("Quantity to Make"),
"fieldtype": "Float",
"default": "1.0",
"reqd": 1
},
{
"fieldname": "show_exploded_view",
"label": __("Show exploded view"),
"fieldtype": "Check"
"fieldtype": "Check",
"default": false,
}
]
}

View File

@@ -4,29 +4,31 @@
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils.data import comma_and
from pypika.terms import ExistsCriterion
def execute(filters=None):
# if not filters: filters = {}
columns = get_columns()
summ_data = []
data = []
data = get_bom_stock(filters)
bom_data = get_bom_data(filters)
qty_to_make = filters.get("qty_to_make")
manufacture_details = get_manufacturer_records()
for row in data:
reqd_qty = qty_to_make * row.actual_qty
last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details))
return columns, summ_data
for row in bom_data:
required_qty = qty_to_make * row.qty_per_unit
last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details))
return columns, data
def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
to_build = row.to_build if row.to_build > 0 else 0
diff_qty = to_build - reqd_qty
def get_report_data(last_purchase_rate, required_qty, row, manufacture_details):
qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0
difference_qty = row.actual_qty - required_qty
return [
row.item_code,
row.description,
@@ -34,85 +36,126 @@ def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
comma_and(
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
),
qty_per_unit,
row.actual_qty,
str(to_build),
reqd_qty,
diff_qty,
last_pur_price,
required_qty,
difference_qty,
last_purchase_rate,
]
def get_columns():
"""return columns"""
columns = [
_("Item") + ":Link/Item:100",
_("Description") + "::150",
_("Manufacturer") + "::250",
_("Manufacturer Part Number") + "::250",
_("Qty") + ":Float:50",
_("Stock Qty") + ":Float:100",
_("Reqd Qty") + ":Float:100",
_("Diff Qty") + ":Float:100",
_("Last Purchase Price") + ":Float:100",
return [
{
"fieldname": "item",
"label": _("Item"),
"fieldtype": "Link",
"options": "Item",
"width": 120,
},
{
"fieldname": "description",
"label": _("Description"),
"fieldtype": "Data",
"width": 150,
},
{
"fieldname": "manufacturer",
"label": _("Manufacturer"),
"fieldtype": "Data",
"width": 120,
},
{
"fieldname": "manufacturer_part_number",
"label": _("Manufacturer Part Number"),
"fieldtype": "Data",
"width": 150,
},
{
"fieldname": "qty_per_unit",
"label": _("Qty Per Unit"),
"fieldtype": "Float",
"width": 110,
},
{
"fieldname": "available_qty",
"label": _("Available Qty"),
"fieldtype": "Float",
"width": 120,
},
{
"fieldname": "required_qty",
"label": _("Required Qty"),
"fieldtype": "Float",
"width": 120,
},
{
"fieldname": "difference_qty",
"label": _("Difference Qty"),
"fieldtype": "Float",
"width": 130,
},
{
"fieldname": "last_purchase_rate",
"label": _("Last Purchase Rate"),
"fieldtype": "Float",
"width": 160,
},
]
return columns
def get_bom_stock(filters):
conditions = ""
bom = filters.get("bom")
table = "`tabBOM Item`"
qty_field = "qty"
def get_bom_data(filters):
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
qty_field = "stock_qty"
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
bom_item = frappe.qb.DocType(bom_item_table)
bin = frappe.qb.DocType("Bin")
query = (
frappe.qb.from_(bom_item)
.left_join(bin)
.on(bom_item.item_code == bin.item_code)
.select(
bom_item.item_code,
bom_item.description,
bom_item.qty_consumed_per_unit.as_("qty_per_unit"),
IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
)
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code)
)
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
conditions += (
" and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
% (warehouse_details.lft, warehouse_details.rgt)
wh = frappe.qb.DocType("Warehouse")
query = query.where(
ExistsCriterion(
frappe.qb.from_(wh)
.select(wh.name)
.where(
(wh.lft >= warehouse_details.lft)
& (wh.rgt <= warehouse_details.rgt)
& (bin.warehouse == wh.name)
)
)
)
else:
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
query = query.where(bin.warehouse == filters.get("warehouse"))
else:
conditions += ""
return frappe.db.sql(
"""
SELECT
bom_item.item_code,
bom_item.description,
bom_item.{qty_field},
ifnull(sum(ledger.actual_qty), 0) as actual_qty,
ifnull(sum(FLOOR(ledger.actual_qty / bom_item.{qty_field})), 0) as to_build
FROM
{table} AS bom_item
LEFT JOIN `tabBin` AS ledger
ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field, table=table, conditions=conditions, bom=bom
),
as_dict=1,
)
return query.run(as_dict=True)
def get_manufacturer_records():
details = frappe.get_all(
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
)
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get("item_code"), {})

View File

@@ -0,0 +1,115 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import (
execute as bom_stock_calculated_report,
)
from erpnext.stock.doctype.item.test_item import make_item
class TestBOMStockCalculated(FrappeTestCase):
def setUp(self):
self.fg_item, self.rm_items = create_items()
self.boms = create_boms(self.fg_item, self.rm_items)
def test_bom_stock_calculated(self):
qty_to_make = 10
# Case 1: When Item(s) Qty and Stock Qty are equal.
data = bom_stock_calculated_report(
filters={
"qty_to_make": qty_to_make,
"bom": self.boms[0].name,
}
)[1]
expected_data = get_expected_data(self.boms[0], qty_to_make)
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
# Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1.
data = bom_stock_calculated_report(
filters={
"qty_to_make": qty_to_make,
"bom": self.boms[1].name,
}
)[1]
expected_data = get_expected_data(self.boms[1], qty_to_make)
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
# Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1.
data = bom_stock_calculated_report(
filters={
"qty_to_make": qty_to_make,
"bom": self.boms[2].name,
}
)[1]
expected_data = get_expected_data(self.boms[2], qty_to_make)
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
def create_items():
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item1 = make_item(
properties={
"is_stock_item": 1,
"standard_rate": 100,
"opening_stock": 100,
"last_purchase_rate": 100,
}
).name
rm_item2 = make_item(
properties={
"is_stock_item": 1,
"standard_rate": 200,
"opening_stock": 200,
"last_purchase_rate": 200,
}
).name
return fg_item, [rm_item1, rm_item2]
def create_boms(fg_item, rm_items):
def update_bom_items(bom, uom, conversion_factor):
for item in bom.items:
item.uom = uom
item.conversion_factor = conversion_factor
return bom
bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10)
bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
bom2 = update_bom_items(bom2, "Box", 10)
bom2.save()
bom2.submit()
bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
bom3 = update_bom_items(bom3, "Box", 10)
bom3.save()
bom3.submit()
return [bom1, bom2, bom3]
def get_expected_data(bom, qty_to_make):
expected_data = []
for idx in range(len(bom.items)):
expected_data.append(
[
bom.items[idx].item_code,
bom.items[idx].item_code,
"",
"",
float(bom.items[idx].stock_qty / bom.quantity),
float(100 * (idx + 1)),
float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)),
float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))),
float(100 * (idx + 1)),
]
)
return expected_data

View File

@@ -4,6 +4,8 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Floor, Sum
from pypika.terms import ExistsCriterion
def execute(filters=None):
@@ -11,7 +13,6 @@ def execute(filters=None):
filters = {}
columns = get_columns()
data = get_bom_stock(filters)
return columns, data
@@ -33,59 +34,57 @@ def get_columns():
def get_bom_stock(filters):
conditions = ""
bom = filters.get("bom")
table = "`tabBOM Item`"
qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0:
qty_to_produce = filters.get("qty_to_produce") or 1
if int(qty_to_produce) < 0:
frappe.throw(_("Quantity to Produce can not be less than Zero"))
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
bin = frappe.qb.DocType("Bin")
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType(bom_item_table)
query = (
frappe.qb.from_(bom)
.inner_join(bom_item)
.on(bom.name == bom_item.parent)
.left_join(bin)
.on(bom_item.item_code == bin.item_code)
.select(
bom_item.item_code,
bom_item.description,
bom_item.stock_qty,
bom_item.stock_uom,
bom_item.stock_qty * qty_to_produce / bom.quantity,
Sum(bin.actual_qty).as_("actual_qty"),
Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))),
)
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code)
)
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
conditions += (
" and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
% (warehouse_details.lft, warehouse_details.rgt)
wh = frappe.qb.DocType("Warehouse")
query = query.where(
ExistsCriterion(
frappe.qb.from_(wh)
.select(wh.name)
.where(
(wh.lft >= warehouse_details.lft)
& (wh.rgt <= warehouse_details.rgt)
& (bin.warehouse == wh.name)
)
)
)
else:
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
query = query.where(bin.warehouse == filters.get("warehouse"))
else:
conditions += ""
return frappe.db.sql(
"""
SELECT
bom_item.item_code,
bom_item.description ,
bom_item.{qty_field},
bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
FROM
`tabBOM` AS bom INNER JOIN {table} AS bom_item
ON bom.name = bom_item.parent
LEFT JOIN `tabBin` AS ledger
ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
bom_item.parent = {bom} and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field,
table=table,
conditions=conditions,
bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1,
)
)
return query.run()

View File

@@ -64,22 +64,21 @@ def get_columns(filters):
def get_data(filters):
cond = "1=1"
wo = frappe.qb.DocType("Work Order")
query = (
frappe.qb.from_(wo)
.select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no)
.where((wo.produced_qty > wo.qty) & (wo.docstatus == 1))
)
if filters.get("bom_no") and not filters.get("work_order"):
cond += " and bom_no = '%s'" % filters.get("bom_no")
query = query.where(wo.bom_no == filters.get("bom_no"))
if filters.get("work_order"):
cond += " and name = '%s'" % filters.get("work_order")
query = query.where(wo.name == filters.get("work_order"))
results = []
for d in frappe.db.sql(
""" select name as work_order, qty, produced_qty, production_item, bom_no
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
cond
),
as_dict=1,
):
for d in query.run(as_dict=True):
results.append(d)
for data in frappe.get_all(
@@ -95,16 +94,17 @@ def get_data(filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
cond = "1=1"
if filters.get("bom_no"):
cond += " and bom_no = '%s'" % filters.get("bom_no")
return frappe.db.sql(
"""select name from `tabWork Order`
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
order by name limit {1}, {2}""".format(
cond, start, page_len
),
{"name": "%%%s%%" % txt},
as_list=1,
wo = frappe.qb.DocType("Work Order")
query = (
frappe.qb.from_(wo)
.select(wo.name)
.where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1))
.orderby(wo.name)
.limit(page_len)
.offset(start)
)
if filters.get("bom_no"):
query = query.where(wo.bom_no == filters.get("bom_no"))
return query.run(as_list=True)

View File

@@ -96,38 +96,39 @@ class ForecastingReport(ExponentialSmoothingForecast):
value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty))
def get_data_for_forecast(self):
cond = ""
if self.filters.item_code:
cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
warehouses = []
if self.filters.warehouse:
warehouses = get_child_warehouses(self.filters.warehouse)
cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
input_data = [self.filters.from_date, self.filters.company]
if warehouses:
input_data.extend(warehouses)
parent = frappe.qb.DocType(self.doctype)
child = frappe.qb.DocType(self.child_doctype)
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
return frappe.db.sql(
"""
SELECT
so.{date_field} as posting_date, soi.item_code, soi.warehouse,
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
FROM
`tab{doc}` so, `tab{child_doc}` soi
WHERE
so.docstatus = 1 AND so.name = soi.parent AND
so.{date_field} < %s AND so.company = %s {cond}
""".format(
doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond
),
tuple(input_data),
as_dict=1,
query = (
frappe.qb.from_(parent)
.from_(child)
.select(
parent[date_field].as_("posting_date"),
child.item_code,
child.warehouse,
child.item_name,
child.stock_qty.as_("qty"),
child.base_amount.as_("amount"),
)
.where(
(parent.docstatus == 1)
& (parent.name == child.parent)
& (parent[date_field] < self.filters.from_date)
& (parent.company == self.filters.company)
)
)
if self.filters.item_code:
query = query.where(child.item_code == self.filters.item_code)
if self.filters.warehouse:
warehouses = get_child_warehouses(self.filters.warehouse) or []
query = query.where(child.warehouse.isin(warehouses))
return query.run(as_dict=True)
def prepare_final_data(self):
self.data = []

View File

@@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
Filters = frappe._dict
Row = frappe._dict
@@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
def execute(filters: Filters) -> Tuple[Columns, Data]:
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters: Filters) -> Data:
query_args = get_query_args(filters)
data = run_query(query_args)
wo = frappe.qb.DocType("Work Order")
se = frappe.qb.DocType("Stock Entry")
query = (
frappe.qb.from_(wo)
.inner_join(se)
.on(wo.name == se.work_order)
.select(
wo.name,
wo.status,
wo.production_item,
wo.qty,
wo.produced_qty,
wo.process_loss_qty,
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
Sum(se.total_incoming_value).as_("total_fg_value"),
Sum(se.total_outgoing_value).as_("total_rm_value"),
)
.where(
(wo.process_loss_qty > 0)
& (wo.company == filters.company)
& (se.docstatus == 1)
& (se.posting_date.between(filters.from_date, filters.to_date))
)
.groupby(se.work_order)
)
if "item" in filters:
query.where(wo.production_item == filters.item)
if "work_order" in filters:
query.where(wo.name == filters.work_order)
data = query.run(as_dict=True)
update_data_with_total_pl_value(data)
return data
@@ -67,54 +103,7 @@ def get_columns() -> Columns:
]
def get_query_args(filters: Filters) -> QueryArgs:
query_args = {}
query_args.update(filters)
query_args.update(get_filter_conditions(filters))
return query_args
def run_query(query_args: QueryArgs) -> Data:
return frappe.db.sql(
"""
SELECT
wo.name, wo.status, wo.production_item, wo.qty,
wo.produced_qty, wo.process_loss_qty,
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
sum(se.total_incoming_value) as total_fg_value,
sum(se.total_outgoing_value) as total_rm_value
FROM
`tabWork Order` wo INNER JOIN `tabStock Entry` se
ON wo.name=se.work_order
WHERE
process_loss_qty > 0
AND wo.company = %(company)s
AND se.docstatus = 1
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
{item_filter}
{work_order_filter}
GROUP BY
se.work_order
""".format(
**query_args
),
query_args,
as_dict=1,
)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
def get_filter_conditions(filters: Filters) -> QueryArgs:
filter_conditions = dict(item_filter="", work_order_filter="")
if "item" in filters:
production_item = filters.get("item")
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
if "work_order" in filters:
work_order_name = filters.get("work_order")
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
return filter_conditions

View File

@@ -4,42 +4,10 @@
import frappe
from frappe import _
from pypika import Order
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
# and bom_no is not null and bom_no !=''
mapper = {
"Sales Order": {
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
`tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
"filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
and `tabSales Order`.per_delivered < 100.0""",
},
"Material Request": {
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
`tabMaterial Request Item`.schedule_date """,
"filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
and `tabMaterial Request`.material_request_type = 'Manufacture' """,
},
"Work Order": {
"fields": """ production_item, item_name as production_item_name, planned_start_date,
stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
},
}
order_mapper = {
"Sales Order": {
"Delivery Date": "`tabSales Order Item`.delivery_date asc",
"Total Amount": "`tabSales Order`.base_grand_total desc",
},
"Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
"Work Order": {"Planned Start Date": "planned_start_date asc"},
}
def execute(filters=None):
return ProductionPlanReport(filters).execute_report()
@@ -63,40 +31,78 @@ class ProductionPlanReport(object):
return self.columns, self.data
def get_open_orders(self):
doctype = (
"`tabWork Order`"
if self.filters.based_on == "Work Order"
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
)
doctype, order_by = self.filters.based_on, self.filters.order_by
filters = mapper.get(self.filters.based_on)["filters"]
filters = self.prepare_other_conditions(filters, self.filters.based_on)
order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
parent = frappe.qb.DocType(doctype)
query = None
self.orders = frappe.db.sql(
""" SELECT {fields} from {doctype}
WHERE {filters} {order_by}""".format(
doctype=doctype,
filters=filters,
order_by=order_by,
fields=mapper.get(self.filters.based_on)["fields"],
),
tuple(self.filters.docnames),
as_dict=1,
)
if doctype == "Work Order":
query = (
frappe.qb.from_(parent)
.select(
parent.production_item,
parent.item_name.as_("production_item_name"),
parent.planned_start_date,
parent.stock_uom,
parent.qty.as_("qty_to_manufacture"),
parent.name,
parent.bom_no,
parent.fg_warehouse.as_("warehouse"),
)
.where(parent.status.notin(["Completed", "Stopped"]))
)
def prepare_other_conditions(self, filters, doctype):
if self.filters.docnames:
field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
if order_by == "Planned Start Date":
query = query.orderby(parent.planned_start_date, order=Order.asc)
if doctype != "Work Order":
filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype)
if self.filters.docnames:
query = query.where(parent.name.isin(self.filters.docnames))
else:
child = frappe.qb.DocType(f"{doctype} Item")
query = (
frappe.qb.from_(parent)
.from_(child)
.select(
child.bom_no,
child.stock_uom,
child.warehouse,
child.parent.as_("name"),
child.item_code.as_("production_item"),
child.stock_qty.as_("qty_to_manufacture"),
child.item_name.as_("production_item_name"),
)
.where(parent.name == child.parent)
)
if self.filters.docnames:
query = query.where(child.parent.isin(self.filters.docnames))
if doctype == "Sales Order":
query = query.select(
child.delivery_date,
parent.base_grand_total,
).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0))
if order_by == "Delivery Date":
query = query.orderby(child.delivery_date, order=Order.asc)
elif order_by == "Total Amount":
query = query.orderby(parent.base_grand_total, order=Order.desc)
elif doctype == "Material Request":
query = query.select(child.schedule_date,).where(
(parent.per_ordered < 100) & (parent.material_request_type == "Manufacture")
)
if order_by == "Required Date":
query = query.orderby(child.schedule_date, order=Order.asc)
query = query.where(parent.docstatus == 1)
if self.filters.company:
filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company))
query = query.where(parent.company == self.filters.company)
return filters
self.orders = query.run(as_dict=True)
def get_raw_materials(self):
if not self.orders:
@@ -134,29 +140,29 @@ class ProductionPlanReport(object):
bom_nos.append(bom_no)
bom_doctype = (
bom_item_doctype = (
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
)
qty_field = (
"qty_consumed_per_unit"
if self.filters.include_subassembly_raw_materials
else "(bom_item.qty / bom.quantity)"
)
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType(bom_item_doctype)
raw_materials = frappe.db.sql(
""" SELECT bom_item.parent, bom_item.item_code,
bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
FROM
`tabBOM` as bom, `tab{1}` as bom_item
WHERE
bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1
""".format(
qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos))
),
tuple(bom_nos),
as_dict=1,
)
if self.filters.include_subassembly_raw_materials:
qty_field = bom_item.qty_consumed_per_unit
else:
qty_field = bom_item.qty / bom.quantity
raw_materials = (
frappe.qb.from_(bom)
.from_(bom_item)
.select(
bom_item.parent,
bom_item.item_code,
bom_item.item_name.as_("raw_material_name"),
qty_field.as_("required_qty_per_unit"),
)
.where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1))
).run(as_dict=True)
if not raw_materials:
return

View File

@@ -3,6 +3,7 @@
import frappe
from frappe.query_builder.functions import IfNull
from frappe.utils import cint
@@ -16,70 +17,70 @@ def execute(filters=None):
def get_item_list(wo_list, filters):
out = []
# Add a row for each item/qty
for wo_details in wo_list:
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
if wo_list:
bin = frappe.qb.DocType("Bin")
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType("BOM Item")
for wo_item_details in frappe.db.get_values(
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
):
# Add a row for each item/qty
for wo_details in wo_list:
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
item_list = frappe.db.sql(
"""SELECT
bom_item.item_code as item_code,
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
FROM
`tabBOM` as bom, `tabBOM Item` AS bom_item
LEFT JOIN `tabBin` AS ledger
ON bom_item.item_code = ledger.item_code
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
WHERE
bom.name = bom_item.parent
and bom_item.item_code = %(item_code)s
and bom.name = %(bom)s
GROUP BY
bom_item.item_code""",
{
"bom": wo_details.bom_no,
"warehouse": wo_item_details.source_warehouse,
"filterhouse": filters.warehouse,
"item_code": wo_item_details.item_code,
},
as_dict=1,
)
for wo_item_details in frappe.db.get_values(
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
):
item_list = (
frappe.qb.from_(bom)
.from_(bom_item)
.left_join(bin)
.on(
(bom_item.item_code == bin.item_code)
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
)
.select(
bom_item.item_code.as_("item_code"),
IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
)
.where(
(bom.name == bom_item.parent)
& (bom_item.item_code == wo_item_details.item_code)
& (bom.name == wo_details.bom_no)
)
.groupby(bom_item.item_code)
).run(as_dict=1)
stock_qty = 0
count = 0
buildable_qty = wo_details.qty
for item in item_list:
count = count + 1
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
stock_qty = stock_qty + 1
elif buildable_qty >= item.build_qty:
buildable_qty = item.build_qty
stock_qty = 0
count = 0
buildable_qty = wo_details.qty
for item in item_list:
count = count + 1
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
stock_qty = stock_qty + 1
elif buildable_qty >= item.build_qty:
buildable_qty = item.build_qty
if count == stock_qty:
build = "Y"
else:
build = "N"
if count == stock_qty:
build = "Y"
else:
build = "N"
row = frappe._dict(
{
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build,
}
)
row = frappe._dict(
{
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build,
}
)
out.append(row)
out.append(row)
return out

View File

@@ -373,3 +373,5 @@ erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
erpnext.patches.v13_0.update_schedule_type_in_loans

View File

@@ -100,6 +100,7 @@ def execute():
"mode_of_payment": loan.mode_of_payment,
"loan_account": loan.loan_account,
"payment_account": loan.payment_account,
"disbursement_account": loan.payment_account,
"interest_income_account": loan.interest_income_account,
"penalty_income_account": loan.penalty_income_account,
},
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account

View File

@@ -0,0 +1,17 @@
import frappe
def execute():
frappe.reload_doc("loan_management", "doctype", "loan")
frappe.reload_doc("loan_management", "doctype", "loan_type")
loan = frappe.qb.DocType("Loan")
loan_type = frappe.qb.DocType("Loan Type")
frappe.qb.update(loan_type).set(
loan_type.repayment_schedule_type, "Monthly as per repayment start date"
).where(loan_type.is_term_loan == 1).run()
frappe.qb.update(loan).set(
loan.repayment_schedule_type, "Monthly as per repayment start date"
).where(loan.is_term_loan == 1).run()

View File

@@ -9,7 +9,7 @@ from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, g
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.hr.utils import get_holiday_dates_for_employee
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
calculate_lwp,

View File

@@ -295,6 +295,7 @@ class TestPayrollEntry(FrappeTestCase):
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
repayment_schedule_type="Monthly as per repayment start date",
)
loan = create_loan(

View File

@@ -587,7 +587,7 @@ class SalarySlip(TransactionBase):
if self.salary_structure:
self.calculate_component_amounts("deductions")
self.set_loan_repayment()
self.add_applicable_loans()
self.set_precision_for_component_amounts()
self.set_net_pay()
@@ -632,9 +632,19 @@ class SalarySlip(TransactionBase):
continue
amount = self.eval_condition_and_formula(struct_row, data)
if (
amount or (struct_row.amount_based_on_formula and amount is not None)
) and struct_row.statistical_component == 0:
if struct_row.statistical_component:
# update statitical component amount in reference data based on payment days
# since row for statistical component is not added to salary slip
if struct_row.depends_on_payment_days:
joining_date, relieving_date = self.get_joining_and_relieving_dates()
default_data[struct_row.abbr] = amount
data[struct_row.abbr] = flt(
(flt(amount) * flt(self.payment_days) / cint(self.total_working_days)),
struct_row.precision("amount"),
)
elif amount or struct_row.amount_based_on_formula and amount is not None:
default_amount = self.eval_condition_and_formula(struct_row, default_data)
self.update_component_row(
struct_row, amount, component_type, data=data, default_amount=default_amount
@@ -687,8 +697,8 @@ class SalarySlip(TransactionBase):
for key in ("earnings", "deductions"):
for d in self.get(key):
default_data[d.abbr] = d.default_amount
data[d.abbr] = d.amount
default_data[d.abbr] = d.default_amount or 0
data[d.abbr] = d.amount or 0
return data, default_data
@@ -1370,44 +1380,47 @@ class SalarySlip(TransactionBase):
return joining_date, relieving_date
def set_loan_repayment(self):
def add_applicable_loans(self):
self.total_loan_repayment = 0
self.total_interest_amount = 0
self.total_principal_amount = 0
self.set("loans", [])
loans = [d.loan for d in self.get("loans")]
for loan in self.get_loan_details():
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
if loan.name not in loans:
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
if (
amounts["interest_amount"] + amounts["payable_principal_amount"]
> amounts["written_off_amount"]
):
if amounts["interest_amount"] > amounts["written_off_amount"]:
amounts["interest_amount"] -= amounts["written_off_amount"]
amounts["written_off_amount"] = 0
else:
amounts["written_off_amount"] -= amounts["interest_amount"]
amounts["interest_amount"] = 0
if (amounts["interest_amount"] or amounts["payable_principal_amount"]) and (
amounts["payable_principal_amount"] + amounts["interest_amount"]
> amounts["written_off_amount"]
):
if amounts["payable_principal_amount"] > amounts["written_off_amount"]:
amounts["payable_principal_amount"] -= amounts["written_off_amount"]
amounts["written_off_amount"] = 0
else:
amounts["written_off_amount"] -= amounts["payable_principal_amount"]
amounts["payable_principal_amount"] = 0
if amounts["interest_amount"] > amounts["written_off_amount"]:
amounts["interest_amount"] -= amounts["written_off_amount"]
amounts["written_off_amount"] = 0
else:
amounts["written_off_amount"] -= amounts["interest_amount"]
amounts["interest_amount"] = 0
if amounts["payable_principal_amount"] > amounts["written_off_amount"]:
amounts["payable_principal_amount"] -= amounts["written_off_amount"]
amounts["written_off_amount"] = 0
else:
amounts["written_off_amount"] -= amounts["payable_principal_amount"]
amounts["payable_principal_amount"] = 0
self.append(
"loans",
{
"loan": loan.name,
"interest_amount": amounts["interest_amount"],
"principal_amount": amounts["payable_principal_amount"],
"loan_account": loan.loan_account,
"interest_income_account": loan.interest_income_account,
},
)
self.append(
"loans",
{
"loan": loan.name,
"interest_amount": amounts["interest_amount"],
"principal_amount": amounts["payable_principal_amount"],
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"]
if not loan.manually_update_paid_amount_in_salary_slip
else 0,
"loan_account": loan.loan_account,
"interest_income_account": loan.interest_income_account,
},
)
for payment in self.get("loans"):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
@@ -1432,7 +1445,14 @@ class SalarySlip(TransactionBase):
def get_loan_details(self):
loan_details = frappe.get_all(
"Loan",
fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
fields=[
"name",
"interest_income_account",
"loan_account",
"loan_type",
"is_term_loan",
"manually_update_paid_amount_in_salary_slip",
],
filters={
"applicant": self.employee,
"docstatus": 1,

View File

@@ -27,6 +27,7 @@ from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
create_exemption_category,
create_payroll_period,
@@ -55,18 +56,7 @@ class TestSalarySlip(FrappeTestCase):
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""",
(month_start_date, month_end_date),
)[0][0]
first_sunday = get_first_sunday()
mark_attendance(emp_id, first_sunday, "Absent", ignore_validate=True) # invalid lwp
mark_attendance(
@@ -273,19 +263,7 @@ class TestSalarySlip(FrappeTestCase):
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""",
(month_start_date, month_end_date),
)[0][0]
first_sunday = get_first_sunday()
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
@@ -338,19 +316,7 @@ class TestSalarySlip(FrappeTestCase):
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
# mark attendance
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""",
(month_start_date, month_end_date),
)[0][0]
first_sunday = get_first_sunday()
mark_attendance(
emp, add_days(first_sunday, 1), "Absent", ignore_validate=True
) # counted as absent
@@ -359,8 +325,8 @@ class TestSalarySlip(FrappeTestCase):
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
salary_slip.start_date = month_start_date
salary_slip.end_date = month_end_date
salary_slip.start_date = get_first_day(nowdate())
salary_slip.end_date = get_last_day(nowdate())
salary_slip.save()
salary_slip.submit()
salary_slip.reload()
@@ -402,18 +368,7 @@ class TestSalarySlip(FrappeTestCase):
)
# mark employee absent for a day since this case works fine if payment days are equal to working days
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql(
"""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""",
(month_start_date, month_end_date),
)[0][0]
first_sunday = get_first_sunday()
mark_attendance(
employee, add_days(first_sunday, 1), "Absent", ignore_validate=True
@@ -1032,6 +987,42 @@ class TestSalarySlip(FrappeTestCase):
components = [row.salary_component for row in new_ss.get("earnings")]
self.assertNotIn("Statistical Component", components)
@change_settings(
"Payroll Settings",
{"payroll_based_on": "Attendance", "consider_unmarked_attendance_as": "Present"},
)
def test_statistical_component_based_on_payment_days(self):
"""
Tests whether component using statistical component in the formula
gets the updated value based on payment days
"""
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
emp = make_employee("test_statistical_component@salary.com")
first_sunday = get_first_sunday()
mark_attendance(emp, add_days(first_sunday, 1), "Absent", ignore_validate=True)
salary_structure = make_salary_structure_for_payment_days_based_component_dependency(
test_statistical_comp=True
)
create_salary_structure_assignment(
emp, salary_structure.name, company="_Test Company", currency="INR"
)
# make salary slip and assert payment days
ss = make_salary_slip_for_payment_days_dependency_test(
"test_statistical_component@salary.com", salary_structure.name
)
amount = precision = None
for entry in ss.earnings:
if entry.salary_component == "Dependency Component":
amount = entry.amount
precision = entry.precision("amount")
break
self.assertEqual(amount, flt((1000 * ss.payment_days / ss.total_working_days) * 0.5, precision))
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
@@ -1151,7 +1142,11 @@ def create_account(account_name, company, parent_account, account_type=None):
def make_earning_salary_component(
setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
setup=False,
test_tax=False,
company_list=None,
include_flexi_benefits=False,
test_statistical_comp=False,
):
data = [
{
@@ -1517,7 +1512,7 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None):
return holiday_list
def make_salary_structure_for_payment_days_based_component_dependency():
def make_salary_structure_for_payment_days_based_component_dependency(test_statistical_comp=False):
earnings = [
{
"salary_component": "Basic Salary - Payment Days",
@@ -1535,6 +1530,27 @@ def make_salary_structure_for_payment_days_based_component_dependency():
"formula": "base * 0.20",
},
]
if test_statistical_comp:
earnings.extend(
[
{
"salary_component": "Statistical Component",
"abbr": "SC",
"type": "Earning",
"statistical_component": 1,
"amount": 1000,
"depends_on_payment_days": 1,
},
{
"salary_component": "Dependency Component",
"abbr": "DC",
"type": "Earning",
"amount_based_on_formula": 1,
"formula": "SC * 0.5",
"depends_on_payment_days": 0,
},
]
)
make_salary_component(earnings, False, company_list=["_Test Company"])

View File

@@ -134,6 +134,7 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.parentfield = parentfield;
new_child_doc.parenttype = doctype;
new_doc[parentfield] = [new_child_doc];
new_doc.project = frm.doc.name;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});

View File

@@ -545,6 +545,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!this.validate_company_and_party()) {
this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
} else {
item.pricing_rules = ''
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_details",
child: item,
@@ -1160,6 +1161,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
uom: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
item.pricing_rules = ''
if(item.item_code && item.uom) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_conversion_factor",
@@ -1236,6 +1238,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
qty: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
item.pricing_rules = ''
this.conversion_factor(doc, cdt, cdn, true);
this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true);

View File

@@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
});
};
erpnext.utils.get_contact_details = function(frm) {
erpnext.utils.get_contact_details = function (frm) {
if (frm.updating_party_details) return;
if (frm.doc["contact_person"]) {
frappe.call({
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
args: {contact: frm.doc.contact_person },
callback: function(r) {
if (r.message)
frm.set_value(r.message);
}
})
args: { contact: frm.doc.contact_person },
callback: function (r) {
if (r.message) frm.set_value(r.message);
},
});
} else {
frm.set_value({
contact_person: "",
contact_display: "",
contact_email: "",
contact_mobile: "",
contact_phone: "",
contact_designation: "",
contact_department: "",
});
}
}
};
erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
if (!value) {

View File

@@ -894,6 +894,7 @@ class GSPConnector:
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
res = None
try:
if request_type == "post":
res = make_post_request(url, headers=headers, data=data)

View File

@@ -47,6 +47,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
}
}
});
},
reverse_charge: function(frm) {
if (frm.doc.reverse_charge == "Y") {
frm.set_value('eligibility_for_itc', 'ITC on Reverse Charge');
}
}
});
}

View File

@@ -257,9 +257,16 @@ def get_regional_address_details(party_details, doctype, company):
update_party_details(party_details, doctype)
customer_gst_category = frappe.get_value(
"Customer", party_details.customer, ["gst_category", "export_type"]
)
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
if is_internal_transfer(party_details, doctype):
if is_internal_transfer(party_details, doctype) or customer_gst_category == (
"SEZ",
"Without Payment of Tax",
):
party_details.taxes_and_charges = ""
party_details.taxes = []
return party_details
@@ -603,6 +610,10 @@ def get_ewb_data(dt, dn):
data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
if is_intrastate_transfer_eway_bill(data):
data.docType = "CHL"
data.subSupplyType = 8
data.itemList = []
data.totalValue = doc.total
@@ -645,6 +656,10 @@ def get_ewb_data(dt, dn):
return data
def is_intrastate_transfer_eway_bill(data):
return data.fromGstin == data.toGstin
@frappe.whitelist()
def generate_ewb_json(dt, dn):
dn = json.loads(dn)
@@ -968,8 +983,6 @@ def validate_reverse_charge_transaction(doc, method):
frappe.throw(msg)
doc.eligibility_for_itc = "ITC on Reverse Charge"
def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")

View File

@@ -15,12 +15,6 @@ filters = filters.concat({
"placeholder":"Company GSTIN",
"options": [""],
"width": "80"
}, {
"fieldname":"invoice_type",
"label": __("Invoice Type"),
"fieldtype": "Select",
"placeholder":"Invoice Type",
"options": ["", "Regular", "SEZ", "Export", "Deemed Export"]
});
// Handle company on change

Some files were not shown because too many files have changed in this diff Show More