Compare commits

..

163 Commits

Author SHA1 Message Date
Pratik Vyas
bc41ce95fc Merge branch 'develop' 2014-11-06 14:36:51 +05:30
Pratik Vyas
4948d336c4 bumped to version 4.11.0 2014-11-06 15:06:51 +06:00
Nabin Hait
be2527d93d Merge pull request #2386 from nabinhait/fix1
Translation fixed for Croatia
2014-11-06 12:52:59 +05:30
Nabin Hait
7f5bb1c8aa Translation fixed for Croatia 2014-11-06 12:51:27 +05:30
Pratik Vyas
a8d40e4409 Merge branch 'develop' 2014-11-05 14:15:47 +05:30
Pratik Vyas
e9b4686fec bumped to version 4.10.0 2014-11-05 14:45:47 +06:00
Nabin Hait
5086ef2499 Update update_requested_and_ordered_qty.py 2014-11-05 12:33:29 +05:30
Nabin Hait
1caca80203 Merge pull request #2378 from nabinhait/fix1
Requested and ordered qty calculation
2014-11-04 18:06:00 +05:30
Nabin Hait
9114c26857 Patch to recalculate requested qty and ordered qty for all items 2014-11-04 15:34:03 +05:30
Nabin Hait
941a965af4 Ordered qty calculation logic 2014-11-04 15:34:03 +05:30
Nabin Hait
4acd431b92 Requested qty calculation logic 2014-11-04 15:34:03 +05:30
Nabin Hait
fad0d566f9 Payment receipt voucher print format fixed 2014-11-04 15:34:03 +05:30
Pratik Vyas
e2d4079363 Merge branch 'develop' 2014-11-03 16:04:30 +05:30
Pratik Vyas
b9ce1f590b bumped to version 4.9.3 2014-11-03 16:34:29 +06:00
Nabin Hait
36463ed790 Merge pull request #2375 from nabinhait/fix1
Check credit limit only if customer debited
2014-11-03 15:55:03 +05:30
Nabin Hait
3c8838816d Check credit limit only if customer debited 2014-11-03 15:49:29 +05:30
Nabin Hait
11e50a8eee Merge pull request #2366 from ankitjavalkarwork/fixprinthideamt
Hide totals in DN, Auto set Bank/Cash acc. in JV
2014-11-03 15:38:45 +05:30
Nabin Hait
9de4c60bd6 Merge pull request #2374 from nabinhait/fix1
minor fix
2014-11-03 15:37:51 +05:30
Nabin Hait
5288bdeabb Minor fix 2014-11-03 15:08:21 +05:30
ankitjavalkarwork
f5804438bb Auto set default bank/cash account on select of Bank/Cash Voucher in JV 2014-11-03 12:23:55 +05:30
ankitjavalkarwork
6e06357dc5 Hide totals on checking print_without_amount field in DN 2014-11-03 11:57:44 +05:30
Nabin Hait
28913b97b0 [fix] Null issue fixed in stock analytics report 2014-10-31 14:46:25 +05:30
Pratik Vyas
c59cd46391 Merge branch 'develop' 2014-10-30 14:22:17 +05:30
Pratik Vyas
dda239fd49 bumped to version 4.9.2 2014-10-30 14:52:17 +06:00
Nabin Hait
b86a6ce26a Merge pull request #2358 from nabinhait/fix1
[fix] Floating point issue fixed in payment reconciliation
2014-10-29 14:26:41 +05:30
Nabin Hait
ee212e7bb5 [fix] Floating point issue fixed in payment reconciliation 2014-10-29 14:26:05 +05:30
Nabin Hait
4952c7b3b5 Merge pull request #2343 from ankitjavalkarwork/fixcustissue
Fix Customer Issue error on click event of Serial No. field
2014-10-27 11:40:18 +05:30
ankitjavalkarwork
4e61536f50 Fix Customer Issue error on click event of Serial No. field 2014-10-27 11:08:05 +05:30
Pratik Vyas
f2886f152b Merge pull request #2348 from pdvyas/fix-pdf-test
Fix PDF test
2014-10-27 10:47:00 +05:30
Pratik Vyas
b5c56f6cea [minor] add port to test site 2014-10-27 09:51:08 +05:30
Pratik Vyas
80e95388f5 Merge branch 'develop' 2014-10-27 09:26:36 +05:30
Pratik Vyas
daed0b655a bumped to version 4.9.1 2014-10-27 09:56:36 +06:00
Nabin Hait
fbb994c731 Merge pull request #2344 from nabinhait/fix1
cost center based on project
2014-10-23 13:30:51 +05:30
Nabin Hait
10b155a486 cost center based on project 2014-10-23 13:18:59 +05:30
Nabin Hait
ce6eda709b Merge pull request #2342 from ankitjavalkarwork/fixtimelogui
Change position of 'To Time' field for better UX
2014-10-22 16:57:52 +05:30
ankitjavalkarwork
14e1d20df3 Change position of 'To Time' field for better UX 2014-10-22 16:55:29 +05:30
Pratik Vyas
da8a02d56e Merge branch 'develop' 2014-10-22 14:19:17 +05:30
Pratik Vyas
16343683d9 bumped to version 4.9.0 2014-10-22 14:49:17 +06:00
Nabin Hait
2bdc017aff Merge pull request #2339 from nabinhait/fix1
Minor fixes
2014-10-22 13:11:21 +05:30
Nabin Hait
16e05c321c Removed stock ageing grid report 2014-10-22 13:10:43 +05:30
Nabin Hait
23d7919865 Stock ageing report fix 2014-10-22 13:06:26 +05:30
Nabin Hait
38265efc39 Minor fix 2014-10-21 20:23:39 +05:30
Rushabh Mehta
2d414706dc [hotfix] translation boo boo 2014-10-21 11:49:40 +05:30
Rushabh Mehta
d85e4b0d46 Merge pull request #2331 from rmehta/translation
[translations] update
2014-10-20 17:12:22 +05:30
Rushabh Mehta
45d03af8f6 [translations] update 2014-10-20 17:11:06 +05:30
Nabin Hait
73a3a2a131 Merge pull request #2327 from nabinhait/stock_reco
Fixes in credit note and quotation
2014-10-20 11:51:01 +05:30
Nabin Hait
73804580d4 validate quotation to lead or customer 2014-10-20 11:18:11 +05:30
Nabin Hait
259f9779db Credit Note: Set total amount and print format fix 2014-10-20 11:18:11 +05:30
Pratik Vyas
14e0e58a7d Merge branch 'develop' 2014-10-17 15:04:05 +05:30
Pratik Vyas
5eb373f7b5 bumped to version 4.8.0 2014-10-17 15:34:05 +06:00
Nabin Hait
4595c30a7b Merge pull request #2322 from nabinhait/stock_reco
Partial payment reconciliation
2014-10-17 15:01:13 +05:30
Nabin Hait
fc2dd44694 Partial payment reconciliation. Fixes #1982 2014-10-17 14:57:20 +05:30
Pratik Vyas
053c54017e [minor] add admin password to travis test site 2014-10-17 14:16:10 +05:30
Pratik Vyas
af473d78f2 Merge branch 'develop' 2014-10-15 15:34:39 +05:30
Pratik Vyas
26d096aa87 bumped to version 4.7.2 2014-10-15 16:04:39 +06:00
Nabin Hait
476c613ba6 Merge pull request #2317 from nabinhait/stock_reco
repost stock utility
2014-10-15 15:33:50 +05:30
Nabin Hait
2348a5f592 repost stock utility 2014-10-15 15:31:33 +05:30
Pratik Vyas
9d8d045c66 Merge branch 'develop' 2014-10-15 14:44:35 +05:30
Pratik Vyas
63914dd55b bumped to version 4.7.1 2014-10-15 15:14:35 +06:00
Nabin Hait
93a8042f08 Merge pull request #2316 from nabinhait/stock_reco
Minor fix
2014-10-15 14:44:06 +05:30
Nabin Hait
b2f32dac1b Minor fix 2014-10-15 14:42:18 +05:30
Pratik Vyas
577a3acaac Merge branch 'develop' 2014-10-15 14:35:47 +05:30
Pratik Vyas
5e46ce8a50 bumped to version 4.7.0 2014-10-15 15:05:47 +06:00
Nabin Hait
3b7342b7b5 Merge pull request #2315 from nabinhait/stock_reco
Strip company in setup wizard
2014-10-15 13:59:07 +05:30
Nabin Hait
531077e504 Strip company in setup wizard 2014-10-15 13:55:37 +05:30
Nabin Hait
147918ed66 Merge pull request #2313 from nabinhait/stock_reco
Fix gl entries for stock transactions
2014-10-15 12:44:59 +05:30
Nabin Hait
9bbfca9226 Fix gl entries for stock transactions 2014-10-15 12:24:38 +05:30
Nabin Hait
f1a07ff105 Allow zero rate while reposting 2014-10-15 12:23:35 +05:30
Nabin Hait
fb6e434315 Guess valuation rate in case of negative stock 2014-10-15 11:34:40 +05:30
Rushabh Mehta
3811d96feb Merge pull request #2308 from devdesco-ceo/patch-1
Update README.md
2014-10-15 10:02:06 +05:30
Viktor Zhuromskyy
1017615d02 Update README.md 2014-10-14 10:12:10 -05:00
Pratik Vyas
056d627f46 Merge branch 'develop' 2014-10-14 17:34:01 +05:30
Pratik Vyas
663bfeacf4 bumped to version 4.6.2 2014-10-14 18:04:01 +06:00
Nabin Hait
e918ebd721 Merge pull request #2307 from nabinhait/stock_reco
Minor fixes
2014-10-14 17:16:32 +05:30
Nabin Hait
0dc18f0102 Minor fixes 2014-10-14 17:15:02 +05:30
Pratik Vyas
8ddc882a66 Merge branch 'develop' 2014-10-14 16:16:29 +05:30
Pratik Vyas
49365d0982 bumped to version 4.6.1 2014-10-14 16:46:29 +06:00
Nabin Hait
50c29c7d0f Merge pull request #2306 from nabinhait/stock_reco
Negative stock in perpetual inventory
2014-10-14 16:10:46 +05:30
Nabin Hait
bf492122f8 minor fix 2014-10-14 16:09:14 +05:30
Nabin Hait
38d0ed9f3a Negative stock allowed for perpetual inventory. Blocked in specific case 2014-10-14 16:08:19 +05:30
Pratik Vyas
248a65b37d Merge branch 'develop' 2014-10-14 12:37:07 +05:30
Pratik Vyas
78f86e9385 bumped to version 4.6.0 2014-10-14 13:07:07 +06:00
Nabin Hait
d9f3e0c275 Merge pull request #2297 from neilLasrado/bom-issue
Fix - BOM calculated wrong cost on update cost
2014-10-14 12:28:16 +05:30
Neil Trini Lasrado
c40451ee2e Patch Fixed 2014-10-14 12:14:39 +05:30
Neil Trini Lasrado
6644406185 Fix - BOM calculated wrong cost on update cost 2014-10-14 12:14:38 +05:30
Nabin Hait
b3a962e121 Merge pull request #2304 from nabinhait/stock_reco
Repost gl entries where mismatch with stock balance
2014-10-14 11:58:41 +05:30
Nabin Hait
8a28ccfa2f Repost gl entries where mismatch with stock balance 2014-10-14 11:41:56 +05:30
Nabin Hait
0a75fa09ef Merge pull request #2300 from nabinhait/stock_reco
Minor fixes
2014-10-13 11:52:00 +05:30
Nabin Hait
17a16eeaf4 Tets case fixed for payment tool 2014-10-13 11:43:14 +05:30
Nabin Hait
3d3f0bcf54 Minor fix in setup wizard 2014-10-13 11:32:41 +05:30
Nabin Hait
bc8b20ae3c Allocate entire advance amount if advance against SO/PO 2014-10-13 10:48:26 +05:30
Nabin Hait
daf344e5fd Precision fixed in batch-wise balance report 2014-10-13 10:48:26 +05:30
Pratik Vyas
7916792f99 Merge pull request #2298 from pdvyas/fix-mariadb
add my_config patch to travis
2014-10-12 19:39:52 +05:30
Pratik Vyas
27e37e68b2 add my_config patch to travis 2014-10-12 19:35:46 +05:30
Nabin Hait
a538f8a24a Merge pull request #2296 from nabinhait/stock_reco
Stock balance report and valuation fixes
2014-10-10 21:23:30 +05:30
Nabin Hait
70ec88b733 fixed test cases 2014-10-10 21:22:46 +05:30
Nabin Hait
4f0e5db216 Stock balance grid report deprecated and moved to server side 2014-10-10 20:54:57 +05:30
Nabin Hait
79ed124939 Update journal_voucher.py 2014-10-10 18:19:03 +05:30
Nabin Hait
c0a3cd603b Merge pull request #2290 from ankitjavalkarwork/accreport
Bug Fixes
2014-10-10 18:11:36 +05:30
Nabin Hait
7c6f990cf9 Minor fix for moving average 2014-10-10 18:03:27 +05:30
Nabin Hait
7820b171d3 Stock balance grid report deprecated and moved to server side 2014-10-10 18:03:07 +05:30
ankitjavalkarwork
0cf4cc283c Add Shipping Addr to Sales Invoice 2014-10-10 16:55:03 +05:30
ankitjavalkarwork
9a4b173b88 Add validation for stopped orders, advance payment in journal voucher 2014-10-10 16:28:41 +05:30
ankitjavalkarwork
6d83454237 Disallow Stopped Orders in Against Voucher table 2014-10-10 13:30:10 +05:30
ankitjavalkarwork
ff231b5e62 Allow advance JV payments in Accounts Receivable/Payable 2014-10-10 13:13:39 +05:30
Nabin Hait
4d74216147 Maintain negative stock balance if balance qty is negative 2014-10-09 19:25:19 +05:30
Nabin Hait
b7e5ad0a31 Merge pull request #2276 from ankitjavalkarwork/sopofield
Rearrange To/From, Recurring Type field for better UX
2014-10-09 11:58:13 +05:30
Pratik Vyas
e435592d64 [minor] Fix default website style patch (reload doc) 2014-10-09 11:22:12 +05:30
Nabin Hait
7ddde8dc3a Merge pull request #2278 from nabinhait/stock_reco
stock reco and repost vouchers
2014-10-08 18:38:54 +05:30
Nabin Hait
fce2881de6 minor fix 2014-10-08 18:38:27 +05:30
Nabin Hait
e96e83d557 stock reco and repost vouchers 2014-10-08 18:06:14 +05:30
ankitjavalkarwork
1b2944e871 Rearrange To/From, Recurring Type field for better UX 2014-10-08 14:24:58 +05:30
Rushabh Mehta
074e73a0dd [translations] updated via translator 2014-10-08 14:16:33 +05:30
Nabin Hait
8923801881 Update stock_entry.py 2014-10-08 13:20:31 +05:30
Nabin Hait
cfafe93391 Merge pull request #2274 from nabinhait/stock_reco
Stock reco
2014-10-08 11:03:03 +05:30
Nabin Hait
6c48ef781b Utility: Repost stock ledger entries and gl entries for all stock transactions 2014-10-08 11:02:18 +05:30
Nabin Hait
adeb976a1b Block negative stock in perpetual inventory 2014-10-08 11:02:18 +05:30
Nabin Hait
bb19b91ef9 stock reco fixes 2014-10-08 11:02:18 +05:30
Nabin Hait
bfa7f171bd Stock reconciliation sl entries 2014-10-08 11:02:18 +05:30
Nabin Hait
b96c014daf Stock Reconciliation logic simplified 2014-10-08 11:02:18 +05:30
Pratik Vyas
e0c83e22d9 Merge branch 'develop' 2014-10-07 17:05:32 +05:30
pdvyas
099ad0f5e1 bumped to version 4.5.2 2014-10-07 17:35:32 +06:00
Nabin Hait
cf9746dd84 Merge pull request #2270 from nabinhait/fix2
Voucher dynamic link in general ledger report
2014-10-07 11:33:40 +05:30
Nabin Hait
b70712dbba Voucher dynamic link in general ledger report 2014-10-07 11:32:54 +05:30
Rushabh Mehta
4c057fe693 Merge pull request #2269 from rmehta/fixes
[fix] packing slip
2014-10-07 11:06:08 +05:30
Nabin Hait
ee8ff51d60 Merge pull request #2261 from ankitjavalkarwork/sopofield
[#2253] Rearrange To/From fields and depend on is_recurring
2014-10-07 10:52:10 +05:30
Rushabh Mehta
9974b16c32 [fix] packing slip 2014-10-07 10:52:05 +05:30
ankitjavalkarwork
9e5f319d80 [#2253] Rearrange To/From fields and depend on is_recurring in SI,SO,PI,PO 2014-10-06 12:58:38 +05:30
Nabin Hait
4f614b4030 Merge pull request #2195 from neilLasrado/manufacture-and-repack
[fix] Issue #2183 - Manufacturing and repack seprated in stock entry
2014-09-30 15:11:05 +05:30
Nabin Hait
3d458e973e Merge pull request #2236 from neilLasrado/upstream/develop
Has Batch No field should be freezed #2023
2014-09-30 14:56:15 +05:30
Neil Trini Lasrado
3a19a71262 patch fixed 2014-09-30 14:48:38 +05:30
Neil Trini Lasrado
29d1a1c593 manufacturing and repack sepreted, test cases fixed, patch fixed 2014-09-30 14:07:56 +05:30
Neil Trini Lasrado
3b90de558f Has Batch No field should be freezed #2023 2014-09-30 12:57:32 +05:30
Nabin Hait
af7e31acb3 Merge pull request #2235 from adityaduggal/develop
Added the dynamic link field in the accounts receivable report.
2014-09-30 10:39:40 +05:30
Aditya Duggal
a2c9d35efb Added the dynamic link field. 2014-09-29 17:44:28 +05:30
Aditya Duggal
4c058f4056 Added the dynamic link and removed the link column 2014-09-29 17:41:19 +05:30
Pratik Vyas
ad67b84d43 Merge branch 'develop' 2014-09-29 15:40:04 +05:30
Pratik Vyas
4e81e4065b bumped to version 4.5.1 2014-09-29 16:10:04 +06:00
Nabin Hait
81332789cb Merge pull request #2232 from nabinhait/hotfix
Fix gl entries for stock transactions
2014-09-29 15:25:02 +05:30
Nabin Hait
82d7c0c9eb Patch: Fix gl entries for stock transactions 2014-09-29 15:07:51 +05:30
Nabin Hait
d60235e239 minor fix in warehouse-wise stock balance report 2014-09-29 11:36:06 +05:30
Nabin Hait
9b50b0a762 Fixes for item list view 2014-09-29 11:36:06 +05:30
Pratik Vyas
21e14c4c98 Merge branch 'develop' 2014-09-26 16:49:59 +05:30
Pratik Vyas
16edacebc7 bumped to version 4.5.0 2014-09-26 17:19:59 +06:00
Nabin Hait
75027b4d54 Merge pull request #2225 from nabinhait/hotfix
GL Entries for future stock voucher and multiple minor fixes
2014-09-26 14:48:56 +05:30
Nabin Hait
b0bd99266d Fix in landed cost voucher 2014-09-26 14:30:02 +05:30
Nabin Hait
b9e04815f8 Repost gl entries for future stock vouchers 2014-09-26 14:24:42 +05:30
Nabin Hait
18ccc27b1b Minor fix in naming naming series 2014-09-26 14:23:00 +05:30
Nabin Hait
95f1fe92e2 Maintenance visit search field issue fixed 2014-09-26 14:23:00 +05:30
Nabin Hait
b783f519ee Fixes in sales/purchase invoice trends report 2014-09-26 14:23:00 +05:30
Nabin Hait
996a1010cb Moved Installation note from tools to documents section 2014-09-26 14:23:00 +05:30
Anand Doshi
32a9dfd983 Merge branch 'sbkolate-develop' into develop 2014-09-22 13:19:24 +05:30
Anand Doshi
1394509343 Fixes for recurring document 2014-09-21 19:45:49 +05:30
Anand Doshi
8370cb3e71 Merge branch 'develop' of https://github.com/sbkolate/erpnext into sbkolate-develop 2014-09-18 12:06:05 +05:30
Sambhaji Kolate
f37d4337a4 fix test for purchase order 2014-09-15 13:37:46 +05:30
Sambhaji Kolate
525ab0a925 fix build 2014-09-15 12:57:58 +05:30
Sambhaji Kolate
930e7f5578 fix conflict 2014-09-15 12:43:48 +05:30
Sambhaji Kolate
d14e15d432 fix conflict 2014-09-15 12:31:44 +05:30
Sambhaji Kolate
4d3a18890b fix conflict 2014-09-15 12:20:11 +05:30
Sambhaji Kolate
b14401c320 change convert_to_recurring() to take recurring_id dynamicaly 2014-09-11 16:09:05 +05:30
Sambhaji Kolate
b2a3f2d386 some minor changes get fixed for PO/PI 2014-09-10 17:40:48 +05:30
Sambhaji Kolate
6b679c45df Updated purchase_invoice.json and purchase_order.json with some missed out changes 2014-09-10 13:57:45 +05:30
Sambhaji Kolate
e3d2643f2b Changes for Recurring PO/PI 2014-09-10 13:07:59 +05:30
127 changed files with 4489 additions and 4466 deletions

View File

@@ -14,6 +14,8 @@ install:
- sudo apt-get update
- sudo apt-get purge -y mysql-common
- sudo apt-get install mariadb-server mariadb-common libmariadbclient-dev
- ./ci/fix-mariadb.sh
- wget http://downloads.sourceforge.net/project/wkhtmltopdf/0.12.1/wkhtmltox-0.12.1_linux-precise-amd64.deb
- sudo dpkg -i wkhtmltox-0.12.1_linux-precise-amd64.deb
- CFLAGS=-O0 pip install git+https://github.com/frappe/frappe.git@develop

View File

@@ -4,7 +4,7 @@
Includes Accounting, Inventory, CRM, Sales, Purchase, Projects, HRMS. Built on Python / MariaDB.
ERPNext is built on [frappe](https://github.com/frappe/frappe)
ERPNext is built on [frappe](https://github.com/frappe/frappe) Python Framework.
- [User Guide](http://erpnext.org/user-guide.html)
- [Getting Help](http://erpnext.org/getting-help.html)
@@ -21,7 +21,7 @@ Use the bench, https://github.com/frappe/bench
1. go to "/login"
1. Administrator user name: "Administrator"
1. Administrator passowrd "admin"
1. Administrator password: "admin"
### Download and Install

11
ci/fix-mariadb.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# stolen from http://cgit.drupalcode.org/octopus/commit/?id=db4f837
includedir=`mysql_config --variable=pkgincludedir`
thiscwd=`pwd`
_THIS_DB_VERSION=`mysql -V 2>&1 | tr -d "\n" | cut -d" " -f6 | awk '{ print $1}' | cut -d"-" -f1 | awk '{ print $1}' | sed "s/[\,']//g"`
if [ "$_THIS_DB_VERSION" = "5.5.40" ] && [ ! -e "$includedir-$_THIS_DB_VERSION-fixed.log" ] ; then
cd $includedir
sudo patch -p1 < $thiscwd/ci/my_config.h.patch &> /dev/null
sudo touch $includedir-$_THIS_DB_VERSION-fixed.log
fi

22
ci/my_config.h.patch Normal file
View File

@@ -0,0 +1,22 @@
diff -burp a/my_config.h b/my_config.h
--- a/my_config.h 2014-10-09 19:32:46.000000000 -0400
+++ b/my_config.h 2014-10-09 19:35:12.000000000 -0400
@@ -641,17 +641,4 @@
#define SIZEOF_TIME_T 8
/* #undef TIME_T_UNSIGNED */
-/*
- stat structure (from <sys/stat.h>) is conditionally defined
- to have different layout and size depending on the defined macros.
- The correct macro is defined in my_config.h, which means it MUST be
- included first (or at least before <features.h> - so, practically,
- before including any system headers).
-
- __GLIBC__ is defined in <features.h>
-*/
-#ifdef __GLIBC__
-#error <my_config.h> MUST be included first!
-#endif
-
#endif

View File

@@ -1 +1 @@
__version__ = '4.4.2'
__version__ = '4.11.0'

View File

@@ -124,6 +124,10 @@ def update_outstanding_amt(account, against_voucher_type, against_voucher, on_ca
from `tabGL Entry` where voucher_type = 'Journal Voucher' and voucher_no = %s
and account = %s and ifnull(against_voucher, '') = ''""",
(against_voucher, account))[0][0])
if not against_voucher_amount:
frappe.throw(_("Against Journal Voucher {0} is already adjusted against some other voucher")
.format(against_voucher))
bal = against_voucher_amount + bal
if against_voucher_amount < 0:
bal = -bal

View File

@@ -213,10 +213,11 @@ cur_frm.cscript.voucher_type = function(doc, cdt, cdn) {
return;
var update_jv_details = function(doc, r) {
$.each(r.message, function(i, d) {
var jvdetail = frappe.model.add_child(doc, "Journal Voucher Detail", "entries");
jvdetail.account = d.account;
jvdetail.balance = d.balance;
var jvdetail = frappe.model.add_child(doc, "Journal Voucher Detail", "entries");
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Voucher Detail", "entries");
row.account = d.account;
row.balance = d.balance;
});
refresh_field("entries");
}
@@ -231,7 +232,7 @@ cur_frm.cscript.voucher_type = function(doc, cdt, cdn) {
},
callback: function(r) {
if(r.message) {
update_jv_details(doc, r);
update_jv_details(doc, [r.message]);
}
}
})
@@ -245,7 +246,7 @@ cur_frm.cscript.voucher_type = function(doc, cdt, cdn) {
callback: function(r) {
frappe.model.clear_table(doc, "entries");
if(r.message) {
update_jv_details(doc, r);
update_jv_details(doc, r.message);
}
cur_frm.set_value("is_opening", "Yes")
}

View File

@@ -76,26 +76,36 @@ class JournalVoucher(AccountsController):
def validate_entries_for_advance(self):
for d in self.get('entries'):
if not d.is_advance and not d.against_voucher and \
not d.against_invoice and not d.against_jv:
if not (d.against_voucher and d.against_invoice and d.against_jv):
master_type = frappe.db.get_value("Account", d.account, "master_type")
if (master_type == 'Customer' and flt(d.credit) > 0) or \
(master_type == 'Supplier' and flt(d.debit) > 0):
msgprint(_("Row {0}: Please check 'Is Advance' against Account {1} if this \
is an advance entry.").format(d.idx, d.account))
if not d.is_advance:
msgprint(_("Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.").format(d.idx, d.account))
elif (d.against_sales_order or d.against_purchase_order) and d.is_advance != "Yes":
frappe.throw(_("Row {0}: Payment against Sales/Purchase Order should always be marked as advance").format(d.idx))
def validate_against_jv(self):
for d in self.get('entries'):
if d.against_jv:
account_root_type = frappe.db.get_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
frappe.throw(_("For {0}, only credit entries can be linked against another debit entry")
.format(d.account))
elif account_root_type == "Liability" and flt(d.credit) > 0:
frappe.throw(_("For {0}, only debit entries can be linked against another credit entry")
.format(d.account))
if d.against_jv == self.name:
frappe.throw(_("You can not enter current voucher in 'Against Journal Voucher' column"))
against_entries = frappe.db.sql("""select * from `tabJournal Voucher Detail`
where account = %s and docstatus = 1 and parent = %s
and ifnull(against_jv, '') = ''""", (d.account, d.against_jv), as_dict=True)
and ifnull(against_jv, '') = '' and ifnull(against_invoice, '') = ''
and ifnull(against_voucher, '') = ''""", (d.account, d.against_jv), as_dict=True)
if not against_entries:
frappe.throw(_("Journal Voucher {0} does not have account {1} or already matched")
frappe.throw(_("Journal Voucher {0} does not have account {1} or already matched against other voucher")
.format(d.against_jv, d.account))
else:
dr_or_cr = "debit" if d.credit > 0 else "credit"
@@ -152,7 +162,7 @@ class JournalVoucher(AccountsController):
and voucher_account != d.account:
frappe.throw(_("Row {0}: Account {1} does not match with {2} {3} account") \
.format(d.idx, d.account, doctype, field_dict.get(doctype)))
if against_field in ["against_sales_order", "against_purchase_order"]:
if voucher_account != account_master_name:
frappe.throw(_("Row {0}: Account {1} does not match with {2} {3} Name") \
@@ -164,7 +174,7 @@ class JournalVoucher(AccountsController):
def validate_against_invoice_fields(self, doctype, payment_against_voucher):
for voucher_no, payment_list in payment_against_voucher.items():
voucher_properties = frappe.db.get_value(doctype, voucher_no,
voucher_properties = frappe.db.get_value(doctype, voucher_no,
["docstatus", "outstanding_amount"])
if voucher_properties[0] != 1:
@@ -176,8 +186,8 @@ class JournalVoucher(AccountsController):
def validate_against_order_fields(self, doctype, payment_against_voucher):
for voucher_no, payment_list in payment_against_voucher.items():
voucher_properties = frappe.db.get_value(doctype, voucher_no,
["docstatus", "per_billed", "advance_paid", "grand_total"])
voucher_properties = frappe.db.get_value(doctype, voucher_no,
["docstatus", "per_billed", "status", "advance_paid", "grand_total"])
if voucher_properties[0] != 1:
frappe.throw(_("{0} {1} is not submitted").format(doctype, voucher_no))
@@ -185,7 +195,10 @@ class JournalVoucher(AccountsController):
if flt(voucher_properties[1]) >= 100:
frappe.throw(_("{0} {1} is fully billed").format(doctype, voucher_no))
if flt(voucher_properties[3]) < flt(voucher_properties[2]) + flt(sum(payment_list)):
if cstr(voucher_properties[2]) == "Stopped":
frappe.throw(_("{0} {1} is stopped").format(doctype, voucher_no))
if flt(voucher_properties[4]) < flt(voucher_properties[3]) + flt(sum(payment_list)):
frappe.throw(_("Advance paid against {0} {1} cannot be greater \
than Grand Total {2}").format(doctype, voucher_no, voucher_properties[3]))
@@ -276,26 +289,25 @@ class JournalVoucher(AccountsController):
def set_print_format_fields(self):
for d in self.get('entries'):
result = frappe.db.get_value("Account", d.account,
["account_type", "master_type"])
acc = frappe.db.get_value("Account", d.account, ["account_type", "master_type"], as_dict=1)
if not result:
continue
if not acc: continue
account_type, master_type = result
if master_type in ['Supplier', 'Customer']:
if acc.master_type in ['Supplier', 'Customer']:
if not self.pay_to_recd_from:
self.pay_to_recd_from = frappe.db.get_value(master_type,
' - '.join(d.account.split(' - ')[:-1]),
master_type == 'Customer' and 'customer_name' or 'supplier_name')
self.pay_to_recd_from = frappe.db.get_value(acc.master_type, ' - '.join(d.account.split(' - ')[:-1]),
acc.master_type == 'Customer' and 'customer_name' or 'supplier_name')
if self.voucher_type in ["Credit Note", "Debit Note"]:
self.set_total_amount(d.debit or d.credit)
if account_type in ['Bank', 'Cash']:
company_currency = get_company_currency(self.company)
amt = flt(d.debit) and d.debit or d.credit
self.total_amount = fmt_money(amt, currency=company_currency)
from frappe.utils import money_in_words
self.total_amount_in_words = money_in_words(amt, company_currency)
if acc.account_type in ['Bank', 'Cash']:
self.set_total_amount(d.debit or d.credit)
def set_total_amount(self, amt):
company_currency = get_company_currency(self.company)
self.total_amount = fmt_money(amt, currency=company_currency)
from frappe.utils import money_in_words
self.total_amount_in_words = money_in_words(amt, company_currency)
def check_credit_days(self):
date_diff = 0
@@ -373,7 +385,7 @@ class JournalVoucher(AccountsController):
for d in self.get("entries"):
master_type, master_name = frappe.db.get_value("Account", d.account,
["master_type", "master_name"])
if master_type == "Customer" and master_name:
if master_type == "Customer" and master_name and flt(d.debit) > 0:
super(JournalVoucher, self).check_credit_limit(d.account)
def get_balance(self):
@@ -528,9 +540,10 @@ def get_against_sales_invoice(doctype, txt, searchfield, start, page_len, filter
(filters["account"], "%%%s%%" % txt, start, page_len))
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select jv.name, jv.posting_date, jv.user_remark
from `tabJournal Voucher` jv, `tabJournal Voucher Detail` jv_detail
where jv_detail.parent = jv.name and jv_detail.account = %s and jv.docstatus = 1
return frappe.db.sql("""select distinct jv.name, jv.posting_date, jv.user_remark
from `tabJournal Voucher` jv, `tabJournal Voucher Detail` jvd
where jvd.parent = jv.name and jvd.account = %s and jv.docstatus = 1
and (ifnull(jvd.against_invoice, '') = '' and ifnull(jvd.against_voucher, '') = '' and ifnull(jvd.against_jv, '') = '' )
and jv.%s like %s order by jv.name desc limit %s, %s""" %
("%s", searchfield, "%s", "%s", "%s"),
(filters["account"], "%%%s%%" % txt, start, page_len))

View File

@@ -19,9 +19,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
]
};
}
});
this.frm.set_query('bank_cash_account', function() {
if(!me.frm.doc.company) {
msgprint(__("Please select company first"));
@@ -35,12 +35,8 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
};
}
});
var help_content = '<i class="icon-hand-right"></i> ' + __("Note") + ':<br>'+
'<ul>' + __("If you are unable to match the exact amount, then amend your Journal Voucher and split rows such that payment amount match the invoice amount.") + '</ul>';
this.frm.set_value("reconcile_help", help_content);
},
get_unreconciled_entries: function() {
var me = this;
return this.frm.call({
@@ -48,12 +44,12 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
method: 'get_unreconciled_entries',
callback: function(r, rt) {
var invoices = [];
$.each(me.frm.doc.payment_reconciliation_invoices || [], function(i, row) {
if (row.invoice_number && !inList(invoices, row.invoice_number))
if (row.invoice_number && !inList(invoices, row.invoice_number))
invoices.push(row.invoice_number);
});
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number",
me.frm.doc.name).options = invoices.join("\n");
@@ -79,4 +75,4 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
$.extend(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
cur_frm.add_fetch('party_account', 'master_type', 'party_type')
cur_frm.add_fetch('party_account', 'master_type', 'party_type')

View File

@@ -118,19 +118,12 @@
"options": "Payment Reconciliation Invoice",
"permlevel": 0,
"read_only": 1
},
{
"fieldname": "reconcile_help",
"fieldtype": "Small Text",
"label": "",
"permlevel": 0,
"read_only": 1
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"modified": "2014-07-31 05:43:03.410832",
"modified": "2014-10-16 17:51:44.367107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",

View File

@@ -96,13 +96,14 @@ class PaymentReconciliation(Document):
payment_amount = payment_amount[0][0] if payment_amount else 0
if d.invoice_amount > payment_amount:
if d.invoice_amount - payment_amount > 0.005:
non_reconciled_invoices.append({
'voucher_no': d.voucher_no,
'voucher_type': d.voucher_type,
'posting_date': d.posting_date,
'invoice_amount': flt(d.invoice_amount),
'outstanding_amount': d.invoice_amount - payment_amount})
'outstanding_amount': flt(d.invoice_amount - payment_amount, 2)
})
self.add_invoice_entries(non_reconciled_invoices)
@@ -124,7 +125,7 @@ class PaymentReconciliation(Document):
dr_or_cr = "credit" if self.party_type == "Customer" else "debit"
lst = []
for e in self.get('payment_reconciliation_payments'):
if e.invoice_type and e.invoice_number:
if e.invoice_type and e.invoice_number and e.allocated_amount:
lst.append({
'voucher_no' : e.journal_voucher,
'voucher_detail_no' : e.voucher_detail_number,
@@ -134,7 +135,7 @@ class PaymentReconciliation(Document):
'is_advance' : e.is_advance,
'dr_or_cr' : dr_or_cr,
'unadjusted_amt' : flt(e.amount),
'allocated_amt' : flt(e.amount)
'allocated_amt' : flt(e.allocated_amount)
})
if lst:
@@ -162,18 +163,23 @@ class PaymentReconciliation(Document):
invoices_to_reconcile = []
for p in self.get("payment_reconciliation_payments"):
if p.invoice_type and p.invoice_number:
if p.invoice_type and p.invoice_number and p.allocated_amount:
invoices_to_reconcile.append(p.invoice_number)
if p.invoice_number not in unreconciled_invoices.get(p.invoice_type, {}):
frappe.throw(_("{0}: {1} not found in Invoice Details table")
.format(p.invoice_type, p.invoice_number))
if p.amount > unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number):
frappe.throw(_("Row {0}: Payment amount must be less than or equals to invoice outstanding amount. Please refer Note below.").format(p.idx))
if flt(p.allocated_amount) > flt(p.amount):
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to JV amount {2}")
.format(p.idx, p.allocated_amount, p.amount))
if flt(p.allocated_amount) > unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number):
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2}")
.format(p.idx, p.allocated_amount, unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number)))
if not invoices_to_reconcile:
frappe.throw(_("Please select Invoice Type and Invoice Number in atleast one row"))
frappe.throw(_("Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row"))
def check_condition(self, dr_or_cr):
cond = self.from_date and " and posting_date >= '" + self.from_date + "'" or ""

View File

@@ -53,11 +53,20 @@
"label": "Column Break",
"permlevel": 0
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated amount",
"permlevel": 0,
"precision": "",
"reqd": 1
},
{
"default": "Sales Invoice",
"fieldname": "invoice_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_list_view": 0,
"label": "Invoice Type",
"options": "\nSales Invoice\nPurchase Invoice\nJournal Voucher",
"permlevel": 0,
@@ -95,7 +104,7 @@
}
],
"istable": 1,
"modified": "2014-07-21 16:53:56.206169",
"modified": "2014-10-16 17:40:54.040194",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",

View File

@@ -91,6 +91,7 @@ def get_orders_to_be_billed(party_type, party_name):
where
%s = %s
and docstatus = 1
and ifnull(status, "") != "Stopped"
and ifnull(grand_total, 0) > ifnull(advance_paid, 0)
and ifnull(per_billed, 0) < 100.0
""" % (voucher_type, 'customer' if party_type == "Customer" else 'supplier', '%s'),

View File

@@ -31,9 +31,10 @@ class TestPaymentTool(unittest.TestCase):
"customer": "_Test Customer 3"
})
jv_against_so1 = self.create_against_jv(jv_test_records[0], {
jv_against_so1 = self.create_against_jv(jv_test_records[0], {
"account": "_Test Customer 3 - _TC",
"against_sales_order": so1.name
"against_sales_order": so1.name,
"is_advance": "Yes"
})
@@ -42,10 +43,11 @@ class TestPaymentTool(unittest.TestCase):
"customer": "_Test Customer 3"
})
jv_against_so2 = self.create_against_jv(jv_test_records[0], {
jv_against_so2 = self.create_against_jv(jv_test_records[0], {
"account": "_Test Customer 3 - _TC",
"against_sales_order": so2.name,
"credit": 1000
"credit": 1000,
"is_advance": "Yes"
})
po = self.create_voucher(po_test_records[1], {
"supplier": "_Test Supplier 1"
@@ -54,20 +56,20 @@ class TestPaymentTool(unittest.TestCase):
#Create SI with partial outstanding
si1 = self.create_voucher(si_test_records[0], {
"customer": "_Test Customer 3",
"debit_to": "_Test Customer 3 - _TC"
"debit_to": "_Test Customer 3 - _TC"
})
jv_against_si1 = self.create_against_jv(jv_test_records[0], {
jv_against_si1 = self.create_against_jv(jv_test_records[0], {
"account": "_Test Customer 3 - _TC",
"against_invoice": si1.name
})
#Create SI with no outstanding
si2 = self.create_voucher(si_test_records[0], {
"customer": "_Test Customer 3",
"debit_to": "_Test Customer 3 - _TC"
"debit_to": "_Test Customer 3 - _TC"
})
jv_against_si2 = self.create_against_jv(jv_test_records[0], {
jv_against_si2 = self.create_against_jv(jv_test_records[0], {
"account": "_Test Customer 3 - _TC",
"against_invoice": si2.name,
"credit": 561.80
@@ -75,7 +77,7 @@ class TestPaymentTool(unittest.TestCase):
pi = self.create_voucher(pi_test_records[0], {
"supplier": "_Test Supplier 1",
"credit_to": "_Test Supplier 1 - _TC"
"credit_to": "_Test Supplier 1 - _TC"
})
#Create a dict containing properties and expected values
@@ -137,7 +139,7 @@ class TestPaymentTool(unittest.TestCase):
payment_tool_doc.set(k, v)
self.check_outstanding_vouchers(payment_tool_doc, args, expected_outstanding)
def check_outstanding_vouchers(self, doc, args, expected_outstanding):
from erpnext.accounts.doctype.payment_tool.payment_tool import get_outstanding_vouchers
@@ -161,7 +163,7 @@ class TestPaymentTool(unittest.TestCase):
new_jv = paytool.make_journal_voucher()
#Create a list of expected values as [party account, payment against, against_jv, against_invoice,
#Create a list of expected values as [party account, payment against, against_jv, against_invoice,
#against_voucher, against_sales_order, against_purchase_order]
expected_values = [
[paytool.party_account, 100.00, expected_outstanding.get("Journal Voucher")[0], None, None, None, None],
@@ -171,7 +173,7 @@ class TestPaymentTool(unittest.TestCase):
[paytool.party_account, 100.00, None, None, None, None, expected_outstanding.get("Purchase Order")[0]]
]
for jv_entry in new_jv.get("entries"):
for jv_entry in new_jv.get("entries"):
if paytool.party_account == jv_entry.get("account"):
row = [
jv_entry.get("account"),
@@ -183,11 +185,11 @@ class TestPaymentTool(unittest.TestCase):
jv_entry.get("against_purchase_order"),
]
self.assertTrue(row in expected_values)
self.assertEquals(new_jv.get("cheque_no"), paytool.reference_no)
self.assertEquals(new_jv.get("cheque_date"), paytool.reference_date)
def clear_table_entries(self):
frappe.db.sql("""delete from `tabGL Entry` where (account = "_Test Customer 3 - _TC" or account = "_Test Supplier 1 - _TC")""")
frappe.db.sql("""delete from `tabSales Order` where customer_name = "_Test Customer 3" """)
frappe.db.sql("""delete from `tabPurchase Order` where supplier_name = "_Test Supplier 1" """)
frappe.db.sql("""delete from `tabPurchase Order` where supplier_name = "_Test Supplier 1" """)

View File

@@ -232,7 +232,6 @@ cur_frm.fields_dict['entries'].grid.get_field('project_name').get_query = functi
}
}
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading

View File

@@ -752,12 +752,133 @@
"print_hide": 1,
"read_only": 0,
"reqd": 0
},
{
"depends_on": "eval:doc.docstatus<2",
"fieldname": "recurring_invoice",
"fieldtype": "Section Break",
"label": "Recurring Invoice",
"options": "icon-time",
"permlevel": 0,
"print_hide": 1
},
{
"fieldname": "column_break_77",
"fieldtype": "Column Break",
"permlevel": 0,
"print_hide": 1,
"width": "50%"
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.docstatus<2",
"description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date",
"fieldname": "is_recurring",
"fieldtype": "Check",
"label": "Is Recurring",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "Select the period when the invoice will be generated automatically",
"fieldname": "recurring_type",
"fieldtype": "Select",
"label": "Recurring Type",
"no_copy": 1,
"options": "Monthly\nQuarterly\nHalf-yearly\nYearly",
"permlevel": 0,
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "Start date of current invoice's period",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"no_copy": 1,
"permlevel": 0
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "End date of current invoice's period",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"no_copy": 1,
"permlevel": 0
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc",
"fieldname": "repeat_on_day_of_month",
"fieldtype": "Int",
"label": "Repeat on Day of Month",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "The date on which recurring invoice will be stop",
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"fieldname": "column_break_82",
"fieldtype": "Column Break",
"permlevel": 0,
"print_hide": 1,
"width": "50%"
},
{
"depends_on": "eval:doc.is_recurring==1",
"description": "The date on which next invoice will be generated. It is generated on submit.",
"fieldname": "next_date",
"fieldtype": "Date",
"label": "Next Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.is_recurring==1",
"description": "The unique id for tracking all recurring invoices. It is generated on submit.",
"fieldname": "recurring_id",
"fieldtype": "Data",
"label": "Recurring Id",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "Enter email id separated by commas, invoice will be mailed automatically on particular date",
"fieldname": "notification_email_address",
"fieldtype": "Small Text",
"label": "Notification Email Address",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2014-09-09 05:35:32.156763",
"modified": "2014-10-08 14:23:20.234176",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -4,11 +4,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, cstr, flt, formatdate
from frappe.utils import cint, cstr, formatdate, flt
from frappe import msgprint, _, throw
from erpnext.setup.utils import get_company_currency
import frappe.defaults
from erpnext.controllers.buying_controller import BuyingController
@@ -250,6 +249,8 @@ class PurchaseInvoice(BuyingController):
reconcile_against_document(lst)
def on_submit(self):
super(PurchaseInvoice, self).on_submit()
self.check_prev_docstatus()
frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,

View File

@@ -231,4 +231,8 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(not frappe.db.sql("""select name from `tabJournal Voucher Detail`
where against_voucher=%s""", pi.name))
def test_recurring_invoice(self):
from erpnext.controllers.tests.test_recurring_document import test_recurring_document
test_recurring_document(self, test_records)
test_records = frappe.get_test_records('Purchase Invoice')

View File

@@ -399,37 +399,6 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
})
}
cur_frm.cscript.is_recurring = function(doc, dt, dn) {
// set default values for recurring invoices
if(doc.is_recurring) {
var owner_email = doc.owner=="Administrator"
? frappe.user_info("Administrator").email
: doc.owner;
doc.notification_email_address = $.map([cstr(owner_email),
cstr(doc.contact_email)], function(v) { return v || null; }).join(", ");
doc.repeat_on_day_of_month = frappe.datetime.str_to_obj(doc.posting_date).getDate();
}
refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
}
cur_frm.cscript.from_date = function(doc, dt, dn) {
// set to_date
if(doc.from_date) {
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
'Yearly': 12};
var months = recurring_type_map[doc.recurring_type];
if(months) {
var to_date = frappe.datetime.add_months(doc.from_date,
months);
doc.to_date = frappe.datetime.add_days(to_date, -1);
refresh_field('to_date');
}
}
}
cur_frm.cscript.send_sms = function() {
frappe.require("assets/erpnext/js/sms_manager.js");
var sms_man = new SMSManager(cur_frm.doc);

View File

@@ -169,28 +169,25 @@
"search_index": 0
},
{
"allow_on_submit": 1,
"depends_on": "",
"description": "Start date of current invoice's period",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From",
"no_copy": 1,
"fieldname": "shipping_address_name",
"fieldtype": "Link",
"hidden": 1,
"in_filter": 1,
"label": "Shipping Address Name",
"options": "Address",
"permlevel": 0,
"print_hide": 0,
"read_only": 0
"precision": "",
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "",
"description": "End date of current invoice's period",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To",
"no_copy": 1,
"fieldname": "shipping_address",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Shipping Address",
"permlevel": 0,
"print_hide": 0,
"read_only": 0
"precision": "",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "currency_section",
@@ -1108,6 +1105,30 @@
"print_hide": 1,
"read_only": 0
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "Start date of current invoice's period",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"read_only": 0
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
"description": "End date of current invoice's period",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"read_only": 0
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
@@ -1120,17 +1141,6 @@
"print_hide": 1,
"read_only": 0
},
{
"depends_on": "eval:doc.is_recurring==1",
"description": "The date on which next invoice will be generated. It is generated on submit.\n",
"fieldname": "next_date",
"fieldtype": "Date",
"label": "Next Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_recurring==1",
@@ -1152,6 +1162,17 @@
"read_only": 0,
"width": "50%"
},
{
"depends_on": "eval:doc.is_recurring==1",
"description": "The date on which next invoice will be generated. It is generated on submit.\n",
"fieldname": "next_date",
"fieldtype": "Date",
"label": "Next Date",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.is_recurring==1",
"description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.",
@@ -1192,7 +1213,7 @@
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2014-09-09 05:35:34.121045",
"modified": "2014-10-10 16:54:22.284284",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -4,18 +4,12 @@
from __future__ import unicode_literals
import frappe
import frappe.defaults
from frappe.utils import add_days, cint, cstr, date_diff, flt, getdate, nowdate, \
get_first_day, get_last_day
from frappe.model.naming import make_autoname
from frappe.utils import cint, cstr, flt
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.controllers.stock_controller import update_gl_entries_after
from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.recurring_document import *
from erpnext.controllers.selling_controller import SellingController
form_grid_templates = {
@@ -78,11 +72,12 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_c_form()
self.validate_time_logs_are_submitted()
validate_recurring_document(self)
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount",
"delivery_note_details")
def on_submit(self):
super(SalesInvoice, self).on_submit()
if cint(self.update_stock) == 1:
self.update_stock_ledger()
else:
@@ -105,7 +100,6 @@ class SalesInvoice(SellingController):
self.update_against_document_in_jv()
self.update_time_log_batch(self.name)
convert_to_recurring(self, "RECINV.#####", self.posting_date)
def before_cancel(self):
self.update_time_log_batch(None)
@@ -146,14 +140,6 @@ class SalesInvoice(SellingController):
'overflow_type': 'delivery'
})
def on_update_after_submit(self):
validate_recurring_document(self)
convert_to_recurring(self, "RECINV.#####", self.posting_date)
def before_recurring(self):
self.aging_date = None
self.due_date = None
def get_portal_page(self):
return "invoice" if self.docstatus==1 else None
@@ -487,9 +473,8 @@ class SalesInvoice(SellingController):
if repost_future_gle and cint(self.update_stock) \
and cint(frappe.defaults.get_global_default("auto_accounting_for_stock")):
items, warehouse_account = self.get_items_and_warehouse_accounts()
update_gl_entries_after(self.posting_date, self.posting_time,
warehouse_account, items)
items, warehouses = self.get_items_and_warehouses()
update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items)
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import merge_similar_entries

View File

@@ -97,8 +97,7 @@ def validate_account_for_auto_accounting_for_stock(gl_map):
for entry in gl_map:
if entry.account in aii_accounts:
frappe.throw(_("Account: {0} can only be updated via \
Stock Transactions").format(entry.account), StockAccountInvalidTransaction)
frappe.throw(_("Account: {0} can only be updated via Stock Transactions").format(entry.account), StockAccountInvalidTransaction)
def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,

View File

@@ -4,9 +4,9 @@
"doc_type": "Journal Voucher",
"docstatus": 0,
"doctype": "Print Format",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"<strong>\" + doc.total_amount + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-sm-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-sm-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"<strong>\" + frappe.utils.cstr(doc.total_amount) + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-sm-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-sm-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n",
"idx": 2,
"modified": "2014-08-29 13:20:15.789533",
"modified": "2014-10-17 17:20:02.740340",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Credit Note",

View File

@@ -3,9 +3,9 @@
"doc_type": "Journal Voucher",
"docstatus": 0,
"doctype": "Print Format",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Payment Receipt Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Received On\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Received From\"), doc.pay_to_recd_from),\n (_(\"Amount\"), \"<strong>\" + doc.total_amount + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n <div class=\"row\">\n <div class=\"col-sm-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-sm-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Payment Receipt Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Received On\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Received From\"), doc.pay_to_recd_from),\n (_(\"Amount\"), \"<strong>\" + doc.total_amount or 0 + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n <div class=\"row\">\n <div class=\"col-sm-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-sm-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n",
"idx": 1,
"modified": "2014-08-29 15:55:34.248384",
"modified": "2014-11-04 11:25:57.560873",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Receipt Voucher",

View File

@@ -30,7 +30,8 @@ def execute(filters=None):
data = []
for gle in entries:
if cstr(gle.against_voucher) == gle.voucher_no or not gle.against_voucher \
or [gle.against_voucher_type, gle.against_voucher] in entries_after_report_date:
or [gle.against_voucher_type, gle.against_voucher] in entries_after_report_date \
or (gle.against_voucher_type == "Purchase Order"):
voucher_details = voucher_detail_map.get(gle.voucher_type, {}).get(gle.voucher_no, {})
invoiced_amount = gle.credit > 0 and gle.credit or 0

View File

@@ -21,7 +21,7 @@ class AccountsReceivableReport(object):
def get_columns(self, customer_naming_by):
columns = [
_("Posting Date") + ":Date:80", _("Account") + ":Link/Account:150",
_("Voucher Type") + "::110", _("Voucher No") + "::120", "::30",
_("Voucher Type") + "::110", _("Voucher No") + ":Dynamic Link/Voucher Type:120",
_("Due Date") + ":Date:80",
_("Invoiced Amount") + ":Currency:100", _("Payment Received") + ":Currency:100",
_("Outstanding Amount") + ":Currency:100", _("Age") + ":Int:50", "0-30:Currency:100",
@@ -63,11 +63,6 @@ class AccountsReceivableReport(object):
row += [self.get_territory(gle.account), gle.remarks]
data.append(row)
for i in range(0, len(data)):
data[i].insert(4, """<a href="%s"><i class="icon icon-share" style="cursor: pointer;"></i></a>""" \
% ("/".join(["#Form", data[i][2], data[i][3]]),))
return data
def get_entries_after(self, report_date):
@@ -84,6 +79,9 @@ class AccountsReceivableReport(object):
return (
# advance
(not gle.against_voucher) or
# against sales order
(gle.against_voucher_type == "Sales Order") or
# sales invoice
(gle.against_voucher==gle.voucher_no and gle.debit > 0) or

View File

@@ -35,7 +35,7 @@ def validate_filters(filters, account_details):
def get_columns():
return [_("Posting Date") + ":Date:100", _("Account") + ":Link/Account:200", _("Debit") + ":Float:100",
_("Credit") + ":Float:100", _("Voucher Type") + "::120", _("Voucher No") + "::160", _("Link") + "::20",
_("Credit") + ":Float:100", _("Voucher Type") + "::120", _("Voucher No") + ":Dynamic Link/Voucher Type:160",
_("Against Account") + "::120", _("Cost Center") + ":Link/Cost Center:100", _("Remarks") + "::400"]
def get_result(filters, account_details):
@@ -162,15 +162,6 @@ def get_result_as_list(data):
for d in data:
result.append([d.get("posting_date"), d.get("account"), d.get("debit"),
d.get("credit"), d.get("voucher_type"), d.get("voucher_no"),
get_voucher_link(d.get("voucher_type"), d.get("voucher_no")),
d.get("against"), d.get("cost_center"), d.get("remarks")])
return result
def get_voucher_link(voucher_type, voucher_no):
icon = ""
if voucher_type and voucher_no:
icon = """<a href="%s"><i class="icon icon-share" style="cursor: pointer;">
</i></a>""" % ("/".join(["#Form", voucher_type, voucher_no]))
return icon

View File

@@ -11,4 +11,4 @@ def execute(filters=None):
conditions = get_columns(filters, "Sales Invoice")
data = get_data(filters, conditions)
return conditions["columns"], data
return conditions["columns"], data

View File

@@ -3,15 +3,13 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, flt
from frappe.utils import flt
from frappe import _
from erpnext.stock.doctype.item.item import get_last_purchase_details
from erpnext.controllers.buying_controller import BuyingController
class PurchaseCommon(BuyingController):
def update_last_purchase_rate(self, obj, is_submit):
"""updates last_purchase_rate in item table for each item"""
@@ -123,27 +121,6 @@ class PurchaseCommon(BuyingController):
else:
chk_dupl_itm.append(f)
def get_qty(self, curr_doctype, ref_tab_fname, ref_tab_dn, ref_doc_tname, transaction, curr_parent_name):
# Get total Quantities of current doctype (eg. PR) except for qty of this transaction
#------------------------------
# please check as UOM changes from Material Request - Purchase Order ,so doing following else uom should be same .
# i.e. in PO uom is NOS then in PR uom should be NOS
# but if in Material Request uom KG it can change in PO
get_qty = (transaction == 'Material Request - Purchase Order') and 'qty * conversion_factor' or 'qty'
qty = frappe.db.sql("""select sum(%s) from `tab%s` where %s = %s and
docstatus = 1 and parent != %s""" % (get_qty, curr_doctype, ref_tab_fname, '%s', '%s'),
(ref_tab_dn, curr_parent_name))
qty = qty and flt(qty[0][0]) or 0
# get total qty of ref doctype
#--------------------
max_qty = frappe.db.sql("""select qty from `tab%s` where name = %s
and docstatus = 1""" % (ref_doc_tname, '%s'), ref_tab_dn)
max_qty = max_qty and flt(max_qty[0][0]) or 0
return cstr(qty)+'~~~'+cstr(max_qty)
def check_for_stopped_status(self, doctype, docname):
stopped = frappe.db.sql("""select name from `tab%s` where name = %s and
status = 'Stopped'""" % (doctype, '%s'), docname)

File diff suppressed because it is too large Load Diff

View File

@@ -96,50 +96,45 @@ class PurchaseOrder(BuyingController):
check_list.append(d.prevdoc_docname)
pc_obj.check_for_stopped_status( d.prevdoc_doctype, d.prevdoc_docname)
def update_requested_qty(self):
material_request_map = {}
for d in self.get("po_details"):
if d.prevdoc_doctype and d.prevdoc_doctype == "Material Request" and d.prevdoc_detail_docname:
material_request_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname)
def update_bin(self, is_submit, is_stopped = 0):
from erpnext.stock.utils import update_bin
pc_obj = frappe.get_doc('Purchase Common')
for d in self.get('po_details'):
#1. Check if is_stock_item == 'Yes'
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes":
# this happens when item is changed from non-stock to stock item
if not d.warehouse:
continue
for mr, mr_item_rows in material_request_map.items():
if mr and mr_item_rows:
mr_obj = frappe.get_doc("Material Request", mr)
ind_qty, po_qty = 0, flt(d.qty) * flt(d.conversion_factor)
if is_stopped:
po_qty = flt(d.qty) > flt(d.received_qty) and \
flt( flt(flt(d.qty) - flt(d.received_qty))*flt(d.conversion_factor)) or 0
if mr_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError)
# No updates in Material Request on Stop / Unstop
if cstr(d.prevdoc_doctype) == 'Material Request' and not is_stopped:
# get qty and pending_qty of prevdoc
curr_ref_qty = pc_obj.get_qty(d.doctype, 'prevdoc_detail_docname',
d.prevdoc_detail_docname, 'Material Request Item',
'Material Request - Purchase Order', self.name)
max_qty, qty, curr_qty = flt(curr_ref_qty.split('~~~')[1]), \
flt(curr_ref_qty.split('~~~')[0]), 0
mr_obj.update_requested_qty(mr_item_rows)
if flt(qty) + flt(po_qty) > flt(max_qty):
curr_qty = flt(max_qty) - flt(qty)
# special case as there is no restriction
# for Material Request - Purchase Order
curr_qty = curr_qty > 0 and curr_qty or 0
else:
curr_qty = flt(po_qty)
def update_ordered_qty(self, po_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
from erpnext.stock.utils import get_bin
ind_qty = -flt(curr_qty)
def _update_ordered_qty(item_code, warehouse):
ordered_qty = frappe.db.sql("""
select sum((po_item.qty - ifnull(po_item.received_qty, 0))*po_item.conversion_factor)
from `tabPurchase Order Item` po_item, `tabPurchase Order` po
where po_item.item_code=%s and po_item.warehouse=%s
and po_item.qty > ifnull(po_item.received_qty, 0) and po_item.parent=po.name
and po.status!='Stopped' and po.docstatus=1""", (item_code, warehouse))
# Update ordered_qty and indented_qty in bin
args = {
"item_code": d.item_code,
"warehouse": d.warehouse,
"ordered_qty": (is_submit and 1 or -1) * flt(po_qty),
"indented_qty": (is_submit and 1 or -1) * flt(ind_qty),
"posting_date": self.transaction_date
}
update_bin(args)
bin_doc = get_bin(item_code, warehouse)
bin_doc.ordered_qty = flt(ordered_qty[0][0]) if ordered_qty else 0
bin_doc.save()
item_wh_list = []
for d in self.get("po_details"):
if (not po_item_rows or d.name in po_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \
and frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse:
item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list:
_update_ordered_qty(item_code, warehouse)
def check_modified_date(self):
mod_db = frappe.db.sql("select modified from `tabPurchase Order` where name = %s",
@@ -152,20 +147,21 @@ class PurchaseOrder(BuyingController):
def update_status(self, status):
self.check_modified_date()
# step 1:=> Set Status
frappe.db.set(self,'status',cstr(status))
# step 2:=> Update Bin
self.update_bin(is_submit = (status == 'Submitted') and 1 or 0, is_stopped = 1)
self.update_requested_qty()
self.update_ordered_qty()
# step 3:=> Acknowledge user
msgprint(_("Status of {0} {1} is now {2}").format(self.doctype, self.name, status))
def on_submit(self):
super(PurchaseOrder, self).on_submit()
purchase_controller = frappe.get_doc("Purchase Common")
self.update_prevdoc_status()
self.update_bin(is_submit = 1, is_stopped = 0)
self.update_requested_qty()
self.update_ordered_qty()
frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
self.company, self.grand_total)
@@ -190,8 +186,13 @@ class PurchaseOrder(BuyingController):
throw(_("Purchase Invoice {0} is already submitted").format(", ".join(submitted)))
frappe.db.set(self,'status','Cancelled')
self.update_prevdoc_status()
self.update_bin( is_submit = 0, is_stopped = 0)
# Must be called after updating ordered qty in Material Request
self.update_requested_qty()
self.update_ordered_qty()
pc_obj.update_last_purchase_rate(self, is_submit = 0)
def on_update(self):

View File

@@ -29,8 +29,7 @@ class TestPurchaseOrder(unittest.TestCase):
frappe.get_doc(pr).insert()
def test_ordered_qty(self):
frappe.db.sql("delete from tabBin")
existing_ordered_qty = self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC")
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
po = frappe.copy_doc(test_records[0]).insert()
@@ -43,8 +42,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.get("po_details")[0].item_code = "_Test Item"
po.submit()
self.assertEquals(frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "ordered_qty"), 10)
self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 10)
pr = make_purchase_receipt(po.name)
@@ -56,8 +54,9 @@ class TestPurchaseOrder(unittest.TestCase):
pr.insert()
pr.submit()
self.assertEquals(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "ordered_qty")), 6.0)
po.load_from_db()
self.assertEquals(po.get("po_details")[0].received_qty, 4)
self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 6)
frappe.db.set_value('Item', '_Test Item', 'tolerance', 50)
@@ -68,8 +67,16 @@ class TestPurchaseOrder(unittest.TestCase):
pr1.insert()
pr1.submit()
self.assertEquals(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "ordered_qty")), 0.0)
po.load_from_db()
self.assertEquals(po.get("po_details")[0].received_qty, 12)
self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty)
pr1.load_from_db()
pr1.cancel()
po.load_from_db()
self.assertEquals(po.get("po_details")[0].received_qty, 4)
self.assertEquals(self._get_ordered_qty("_Test Item", "_Test Warehouse - _TC"), existing_ordered_qty + 6)
def test_make_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
@@ -107,7 +114,14 @@ class TestPurchaseOrder(unittest.TestCase):
po.get("po_details")[0].qty = 3.4
self.assertRaises(UOMMustBeIntegerError, po.insert)
def test_recurring_order(self):
from erpnext.controllers.tests.test_recurring_document import test_recurring_document
test_recurring_document(self, test_records)
test_dependencies = ["BOM"]
def _get_ordered_qty(self, item_code, warehouse):
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "ordered_qty"))
test_dependencies = ["BOM", "Item Price"]
test_records = frappe.get_test_records('Purchase Order')

View File

@@ -26,6 +26,11 @@ def get_data():
"name": "Purchase Receipt",
"description": _("Goods received from Suppliers."),
},
{
"type": "doctype",
"name": "Installation Note",
"description": _("Installation record for a Serial No.")
},
{
"type": "doctype",
"name": "Item",
@@ -57,11 +62,6 @@ def get_data():
"name": "Stock Reconciliation",
"description": _("Upload stock balance via csv.")
},
{
"type": "doctype",
"name": "Installation Note",
"description": _("Installation record for a Serial No.")
},
{
"type": "doctype",
"name": "Packing Slip",
@@ -142,10 +142,10 @@ def get_data():
"doctype": "Item",
},
{
"type": "page",
"name": "stock-balance",
"label": _("Stock Balance"),
"icon": "icon-table",
"type": "report",
"is_query_report": True,
"name": "Stock Balance",
"doctype": "Warehouse"
},
{
"type": "report",
@@ -170,13 +170,7 @@ def get_data():
"name": "stock-analytics",
"label": _("Stock Analytics"),
"icon": "icon-bar-chart"
},
{
"type": "report",
"is_query_report": True,
"name": "Warehouse-Wise Stock Balance",
"doctype": "Warehouse"
},
}
]
},
{

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, today, flt
from erpnext.setup.utils import get_company_currency, get_exchange_rate
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
from erpnext.utilities.transaction_base import TransactionBase
from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document
import json
class AccountsController(TransactionBase):
@@ -22,6 +23,24 @@ class AccountsController(TransactionBase):
self.validate_for_freezed_account()
if self.meta.get_field("is_recurring"):
validate_recurring_document(self)
def on_submit(self):
if self.meta.get_field("is_recurring"):
convert_to_recurring(self, self.get("posting_date") or self.get("transaction_date"))
def on_update_after_submit(self):
if self.meta.get_field("is_recurring"):
validate_recurring_document(self)
convert_to_recurring(self, self.get("posting_date") or self.get("transaction_date"))
def before_recurring(self):
self.fiscal_year = None
for fieldname in ("due_date", "aging_date"):
if self.meta.get_field(fieldname):
self.set(fieldname, None)
def set_missing_values(self, for_validate=False):
for fieldname in ["posting_date", "transaction_date"]:
if not self.get(fieldname) and self.meta.get_field(fieldname):
@@ -368,7 +387,7 @@ class AccountsController(TransactionBase):
res = frappe.db.sql("""
select
t1.name as jv_no, t1.remark, t2.%s as amount, t2.name as jv_detail_no
t1.name as jv_no, t1.remark, t2.%s as amount, t2.name as jv_detail_no, `against_%s` as against_order
from
`tabJournal Voucher` t1, `tabJournal Voucher Detail` t2
where
@@ -381,7 +400,7 @@ class AccountsController(TransactionBase):
and ifnull(t2.against_purchase_order, '') = ''
) %s)
order by t1.posting_date""" %
(dr_or_cr, '%s', cond),
(dr_or_cr, against_order_field, '%s', cond),
tuple([account_head] + so_list), as_dict= True)
self.set(parentfield, [])
@@ -392,7 +411,7 @@ class AccountsController(TransactionBase):
"jv_detail_no": d.jv_detail_no,
"remarks": d.remark,
"advance_amount": flt(d.amount),
"allocate_amount": 0
"allocated_amount": flt(d.amount) if d.against_order else 0
})
def validate_advance_jv(self, advance_table_fieldname, against_order_field):

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import flt, rounded
from erpnext.setup.utils import get_company_currency
from erpnext.accounts.party import get_party_details
@@ -255,8 +256,6 @@ class BuyingController(StockController):
rm.required_qty = required_qty
rm.conversion_factor = item.conversion_factor
rm.rate = bom_item.rate
rm.amount = required_qty * flt(bom_item.rate)
rm.idx = rm_supplied_idx
if self.doctype == "Purchase Receipt":
@@ -267,7 +266,25 @@ class BuyingController(StockController):
rm_supplied_idx += 1
raw_materials_cost += required_qty * flt(bom_item.rate)
# get raw materials rate
if self.doctype == "Purchase Receipt":
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
"item_code": bom_item.item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * required_qty,
"serial_no": rm.serial_no
})
if not rm.rate:
from erpnext.stock.stock_ledger import get_valuation_rate
rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse)
else:
rm.rate = bom_item.rate
rm.amount = required_qty * flt(rm.rate)
raw_materials_cost += flt(rm.amount)
if self.doctype == "Purchase Receipt":
item.rm_supp_cost = raw_materials_cost

View File

@@ -2,15 +2,28 @@ from __future__ import unicode_literals
import frappe
import frappe.utils
import frappe.defaults
from frappe.utils import cint, cstr, getdate, nowdate, get_first_day, get_last_day
from frappe.utils import add_days, cint, cstr, date_diff, flt, getdate, nowdate, \
get_first_day, get_last_day, comma_and
from frappe.model.naming import make_autoname
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
date_field_map = {
"Sales Order": "transaction_date",
"Sales Invoice": "posting_date",
"Purchase Order": "transaction_date",
"Purchase Invoice": "posting_date"
}
def create_recurring_documents():
manage_recurring_documents("Sales Order")
manage_recurring_documents("Sales Invoice")
manage_recurring_documents("Purchase Order")
manage_recurring_documents("Purchase Invoice")
def manage_recurring_documents(doctype, next_date=None, commit=True):
"""
@@ -19,10 +32,7 @@ def manage_recurring_documents(doctype, next_date=None, commit=True):
"""
next_date = next_date or nowdate()
if doctype == "Sales Order":
date_field = "transaction_date"
elif doctype == "Sales Invoice":
date_field = "posting_date"
date_field = date_field_map[doctype]
recurring_documents = frappe.db.sql("""select name, recurring_id
from `tab{}` where ifnull(is_recurring, 0)=1
@@ -51,7 +61,8 @@ def manage_recurring_documents(doctype, next_date=None, commit=True):
frappe.db.sql("update `tab%s` \
set is_recurring = 0 where name = %s" % (doctype, '%s'),
(ref_document))
notify_errors(ref_document, doctype, ref_wrapper.customer, ref_wrapper.owner)
notify_errors(ref_document, doctype, ref_wrapper.get("customer") or ref_wrapper.get("supplier"),
ref_wrapper.owner)
frappe.db.commit()
exception_list.append(frappe.get_traceback())
@@ -118,7 +129,7 @@ def send_notification(new_rv):
"fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True)
}])
def notify_errors(doc, doctype, customer, owner):
def notify_errors(doc, doctype, party, owner):
from frappe.utils.user import get_system_managers
recipients = get_system_managers(only_name=True)
@@ -127,7 +138,7 @@ def notify_errors(doc, doctype, customer, owner):
message = frappe.get_template("templates/emails/recurring_document_failed.html").render({
"type": doctype,
"name": doc,
"customer": customer
"party": party
}))
assign_task_to_owner(doc, doctype, "Recurring Invoice Failed", recipients)
@@ -155,18 +166,18 @@ def validate_recurring_document(doc):
elif not (doc.from_date and doc.to_date):
throw(_("Period From and Period To dates mandatory for recurring %s") % doc.doctype)
def convert_to_recurring(doc, autoname, posting_date):
if doc.is_recurring:
if not doc.recurring_id:
frappe.db.set(doc, "recurring_id",
make_autoname(autoname))
#
def convert_to_recurring(doc, posting_date):
if doc.is_recurring:
if not doc.recurring_id:
frappe.db.set(doc, "recurring_id", doc.name)
set_next_date(doc, posting_date)
set_next_date(doc, posting_date)
elif doc.recurring_id:
frappe.db.sql("""update `tab%s`
set is_recurring = 0
where recurring_id = %s""" % (doc.doctype, '%s'), (doc.recurring_id))
elif doc.recurring_id:
frappe.db.sql("""update `tab%s` set is_recurring = 0
where recurring_id = %s""" % (doc.doctype, '%s'), (doc.recurring_id))
#
def validate_notification_email_id(doc):
if doc.notification_email_address:

View File

@@ -8,7 +8,7 @@ from frappe import msgprint, _
import frappe.defaults
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries
from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map
class StockController(AccountsController):
def make_gl_entries(self, repost_future_gle=True):
@@ -16,20 +16,20 @@ class StockController(AccountsController):
delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")):
warehouse_account = self.get_warehouse_account()
warehouse_account = get_warehouse_account()
if self.docstatus==1:
gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries)
if repost_future_gle:
items, warehouse_account = self.get_items_and_warehouse_accounts(warehouse_account)
update_gl_entries_after(self.posting_date, self.posting_time,
warehouse_account, items)
items, warehouses = self.get_items_and_warehouses()
update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items,
warehouse_account)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):
from erpnext.accounts.general_ledger import process_gl_map
if not warehouse_account:
warehouse_account = get_warehouse_account()
@@ -88,10 +88,8 @@ class StockController(AccountsController):
return details
def get_items_and_warehouse_accounts(self, warehouse_account=None):
def get_items_and_warehouses(self):
items, warehouses = [], []
if not warehouse_account:
warehouse_account = get_warehouse_account()
if hasattr(self, "fname"):
item_doclist = self.get(self.fname)
@@ -117,86 +115,17 @@ class StockController(AccountsController):
if d.get("t_warehouse") and d.t_warehouse not in warehouses:
warehouses.append(d.t_warehouse)
warehouse_account = {wh: warehouse_account[wh] for wh in warehouses
if warehouse_account.get(wh)}
return items, warehouse_account
return items, warehouses
def get_stock_ledger_details(self):
stock_ledger = {}
for sle in frappe.db.sql("""select warehouse, stock_value_difference, voucher_detail_no
for sle in frappe.db.sql("""select warehouse, stock_value_difference,
voucher_detail_no, item_code, posting_date, actual_qty
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
(self.doctype, self.name), as_dict=True):
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger
def get_warehouse_account(self):
warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount
where account_type = 'Warehouse' and ifnull(master_name, '') != ''"""))
return warehouse_account
def update_gl_entries_after(self, warehouse_account=None):
future_stock_vouchers = self.get_future_stock_vouchers()
gle = self.get_voucherwise_gl_entries(future_stock_vouchers)
if not warehouse_account:
warehouse_account = self.get_warehouse_account()
for voucher_type, voucher_no in future_stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
matched = True
if existing_gle:
for entry in expected_gle:
for e in existing_gle:
if entry.account==e.account \
and entry.against_account==e.against_account\
and entry.cost_center==e.cost_center:
if entry.debit != e.debit or entry.credit != e.credit:
matched = False
break
else:
matched = False
if not matched:
self.delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(repost_future_gle=False)
else:
self.delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(self):
condition = ""
item_list = []
if getattr(self, "fname", None):
item_list = [d.item_code for d in self.get(self.fname)]
if item_list:
condition = "and item_code in ({})".format(", ".join(["%s"] * len(item_list)))
future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, name asc""".format(
condition=condition), tuple([self.posting_date, self.posting_date] + item_list),
as_list=True)
return future_stock_vouchers
def get_voucherwise_gl_entries(self, future_stock_vouchers):
gl_entries = {}
if future_stock_vouchers:
for d in frappe.db.sql("""select * from `tabGL Entry`
where posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s']*len(future_stock_vouchers))),
tuple([self.posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
def delete_gl_entries(self, voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
def make_adjustment_entry(self, expected_gle, voucher_obj):
from erpnext.accounts.utils import get_stock_and_account_difference
account_list = [d.account for d in expected_gle]
@@ -246,7 +175,7 @@ class StockController(AccountsController):
_(self.doctype), self.name, item.get("item_code")))
def get_sl_entries(self, d, args):
sl_dict = {
sl_dict = frappe._dict({
"item_code": d.get("item_code", None),
"warehouse": d.get("warehouse", None),
"posting_date": self.posting_date,
@@ -263,7 +192,7 @@ class StockController(AccountsController):
"serial_no": d.get("serial_no"),
"project": d.get("project_name"),
"is_cancelled": self.docstatus==2 and "Yes" or "No"
}
})
sl_dict.update(args)
return sl_dict
@@ -287,15 +216,16 @@ class StockController(AccountsController):
return serialized_items
def update_gl_entries_after(posting_date, posting_time, warehouse_account=None, for_items=None):
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
if not warehouse_account:
warehouse_account = get_warehouse_account()
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time,
warehouse_account, for_items)
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
for voucher_type, voucher_no in future_stock_vouchers:
@@ -321,7 +251,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle):
break
return matched
def get_future_stock_vouchers(posting_date, posting_time, warehouse_account=None, for_items=None):
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None):
future_stock_vouchers = []
values = []
@@ -330,9 +260,9 @@ def get_future_stock_vouchers(posting_date, posting_time, warehouse_account=None
condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items)))
values += for_items
if warehouse_account:
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(warehouse_account.keys())))
values += warehouse_account.keys()
if for_warehouses:
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle

View File

@@ -2,12 +2,8 @@
# License: GNU General Public License v3. See license.txt
import frappe
import unittest, json, copy
from frappe.utils import flt
import frappe.permissions
from erpnext.accounts.utils import get_stock_and_account_difference
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.projects.doctype.time_log_batch.test_time_log_batch import *
from erpnext.controllers.recurring_document import date_field_map
def test_recurring_document(obj, test_records):
from frappe.utils import get_first_day, get_last_day, add_to_date, nowdate, getdate, add_days
@@ -27,20 +23,11 @@ def test_recurring_document(obj, test_records):
"to_date": get_last_day(today)
})
if base_doc.doctype == "Sales Order":
base_doc.update({
"transaction_date": today,
"delivery_date": add_days(today, 15)
})
elif base_doc.doctype == "Sales Invoice":
base_doc.update({
"posting_date": today
})
date_field = date_field_map[base_doc.doctype]
base_doc.set(date_field, today)
if base_doc.doctype == "Sales Order":
date_field = "transaction_date"
elif base_doc.doctype == "Sales Invoice":
date_field = "posting_date"
base_doc.set("delivery_date", add_days(today, 15))
# monthly
doc1 = frappe.copy_doc(base_doc)
@@ -128,7 +115,7 @@ def _test_recurring_document(obj, base_doc, date_field, first_and_last_day):
next_date = get_next_date(base_doc.get(date_field), no_of_months,
base_doc.repeat_on_day_of_month)
manage_recurring_documents(base_doc.doctype, next_date=next_date, commit=False)
recurred_documents = frappe.db.sql("""select name from `tab%s`

View File

@@ -4,7 +4,7 @@ app_publisher = "Web Notes Technologies Pvt. Ltd. and Contributors"
app_description = "Open Source Enterprise Resource Planning for Small and Midsized Organizations"
app_icon = "icon-th"
app_color = "#e74c3c"
app_version = "4.4.2"
app_version = "4.11.0"
error_report_email = "support@erpnext.com"
@@ -47,8 +47,8 @@ doc_events = {
"on_update": "erpnext.home.make_comment_feed"
},
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_qty"
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty"
},
"User": {
"validate": "erpnext.hr.doctype.employee.employee.validate_employee_role",

View File

@@ -115,6 +115,9 @@ class BOM(Document):
return rate
def update_cost(self):
if self.docstatus == 2:
return
for d in self.get("bom_materials"):
d.rate = self.get_bom_material_detail({
'item_code': d.item_code,
@@ -122,9 +125,10 @@ class BOM(Document):
'qty': d.qty
})["rate"]
if self.docstatus in (0, 1):
if self.docstatus == 1:
self.ignore_validate_update_after_submit = True
self.save()
self.calculate_cost()
self.save()
def get_bom_unitcost(self, bom_no):
bom = frappe.db.sql("""select name, total_variable_cost/quantity as unit_cost from `tabBOM`
@@ -269,29 +273,27 @@ class BOM(Document):
"""Calculate bom totals"""
self.calculate_op_cost()
self.calculate_rm_cost()
self.calculate_fixed_cost()
self.total_variable_cost = self.raw_material_cost + self.operating_cost
self.total_cost = self.total_variable_cost + self.total_fixed_cost
def calculate_op_cost(self):
"""Update workstation rate and calculates totals"""
total_op_cost = 0
total_op_cost, fixed_cost = 0, 0
for d in self.get('bom_operations'):
if d.workstation and not d.hour_rate:
d.hour_rate = frappe.db.get_value("Workstation", d.workstation, "hour_rate")
if d.workstation:
w = frappe.db.get_value("Workstation", d.workstation, ["hour_rate", "fixed_cycle_cost"])
if not d.hour_rate:
d.hour_rate = flt(w[0])
fixed_cost += flt(w[1])
if d.hour_rate and d.time_in_mins:
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
total_op_cost += flt(d.operating_cost)
self.operating_cost = total_op_cost
def calculate_fixed_cost(self):
"""Update workstation rate and calculates totals"""
fixed_cost = 0
for d in self.get('bom_operations'):
if d.workstation:
fixed_cost += flt(frappe.db.get_value("Workstation", d.workstation, "fixed_cycle_cost"))
self.total_fixed_cost = fixed_cost
def calculate_rm_cost(self):
"""Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0

View File

@@ -96,7 +96,7 @@ cur_frm.cscript['Transfer Raw Materials'] = function() {
}
cur_frm.cscript['Update Finished Goods'] = function() {
cur_frm.cscript.make_se('Manufacture/Repack');
cur_frm.cscript.make_se('Manufacture');
}
cur_frm.fields_dict['production_item'].get_query = function(doc) {

View File

@@ -109,15 +109,15 @@
"permlevel": 0
},
{
"depends_on": "eval:doc.docstatus==1",
"description": "Automatically updated via Stock Entry of type Manufacture/Repack",
"fieldname": "produced_qty",
"fieldtype": "Float",
"label": "Manufactured Qty",
"no_copy": 1,
"oldfieldname": "produced_qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"depends_on": "eval:doc.docstatus==1",
"description": "Automatically updated via Stock Entry of type Manufacture or Repack",
"fieldname": "produced_qty",
"fieldtype": "Float",
"label": "Manufactured Qty",
"no_copy": 1,
"oldfieldname": "produced_qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"read_only": 1
},
{

View File

@@ -103,7 +103,7 @@ class ProductionOrder(Document):
status = "Submitted"
if stock_entries:
status = "In Process"
produced_qty = stock_entries.get("Manufacture/Repack")
produced_qty = stock_entries.get("Manufacture")
if flt(produced_qty) == flt(self.qty):
status = "Completed"
@@ -113,7 +113,7 @@ class ProductionOrder(Document):
def update_produced_qty(self):
produced_qty = frappe.db.sql("""select sum(fg_completed_qty)
from `tabStock Entry` where production_order=%s and docstatus=1
and purpose='Manufacture/Repack'""", self.name)
and purpose='Manufacture'""", self.name)
produced_qty = flt(produced_qty[0][0]) if produced_qty else 0
if produced_qty > self.qty:

View File

@@ -31,7 +31,7 @@ class TestProductionOrder(unittest.TestCase):
s.submit()
# from wip to fg
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture/Repack", 4))
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture", 4))
s.insert()
s.submit()
@@ -49,7 +49,7 @@ class TestProductionOrder(unittest.TestCase):
test_stock_entry.make_stock_entry("_Test Item", None, "_Test Warehouse - _TC", 100, 100)
test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "_Test Warehouse - _TC", 100, 100)
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture/Repack", 7))
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture", 7))
s.insert()
self.assertRaises(StockOverProductionError, s.submit)

View File

@@ -6,12 +6,12 @@
"doctype": "Report",
"idx": 1,
"is_standard": "Yes",
"modified": "2014-06-03 07:18:17.082436",
"modified": "2014-09-17 12:41:55.740299",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Issued Items Against Production Order",
"owner": "Administrator",
"query": "select\n ste.production_order as \"Production Order:Link/Production Order:120\",\n ste.posting_date as \"Issue Date:Date:140\",\n ste_item.item_code as \"Item Code:Link/Item:120\",\n\tste_item.description as \"Description::150\",\n\tste_item.transfer_qty as \"Qty:Float:100\",\n\tste_item.stock_uom as \"UOM:Link/UOM:80\",\n\tste_item.amount as \"Amount:Currency:120\",\n\tste_item.serial_no as \"Serial No:Link/Serial No:80\",\n\tste_item.s_warehouse as \"Source Warehouse:Link/Warehouse:120\",\n\tste_item.t_warehouse as \"Target Warehouse:Link/Warehouse:120\",\n\tpro.production_item as \"Finished Goods:Link/Item:120\", \n\tste.name as \"Stock Entry:Link/Stock Entry:120\"\nfrom\n\t`tabStock Entry` ste, `tabStock Entry Detail` ste_item, `tabProduction Order` pro\nwhere\n\tifnull(ste.production_order, '') != '' and ste.name = ste_item.parent \n\tand ste.production_order = pro.name and ste.docstatus = 1 \n\tand ste.purpose = 'Manufacture/Repack'\norder by ste.posting_date, ste.production_order, ste_item.item_code",
"query": "select\n ste.production_order as \"Production Order:Link/Production Order:120\",\n ste.posting_date as \"Issue Date:Date:140\",\n ste_item.item_code as \"Item Code:Link/Item:120\",\n\tste_item.description as \"Description::150\",\n\tste_item.transfer_qty as \"Qty:Float:100\",\n\tste_item.stock_uom as \"UOM:Link/UOM:80\",\n\tste_item.amount as \"Amount:Currency:120\",\n\tste_item.serial_no as \"Serial No:Link/Serial No:80\",\n\tste_item.s_warehouse as \"Source Warehouse:Link/Warehouse:120\",\n\tste_item.t_warehouse as \"Target Warehouse:Link/Warehouse:120\",\n\tpro.production_item as \"Finished Goods:Link/Item:120\", \n\tste.name as \"Stock Entry:Link/Stock Entry:120\"\nfrom\n\t`tabStock Entry` ste, `tabStock Entry Detail` ste_item, `tabProduction Order` pro\nwhere\n\tifnull(ste.production_order, '') != '' and ste.name = ste_item.parent \n\tand ste.production_order = pro.name and ste.docstatus = 1 \n\tand ste.purpose = 'Manufacture' or 'Repack'\norder by ste.posting_date, ste.production_order, ste_item.item_code",
"ref_doctype": "Production Order",
"report_name": "Issued Items Against Production Order",
"report_type": "Query Report"

View File

@@ -80,4 +80,11 @@ execute:frappe.delete_doc("DocType", "Landed Cost Wizard")
erpnext.patches.v4_2.default_website_style
erpnext.patches.v4_2.set_company_country
erpnext.patches.v4_2.update_sales_order_invoice_field_name
erpnext.patches.v4_2.cost_of_production_cycle
erpnext.patches.v4_2.cost_of_production_cycle
erpnext.patches.v4_2.seprate_manufacture_and_repack
execute:frappe.delete_doc("Report", "Warehouse-Wise Stock Balance")
execute:frappe.delete_doc("DocType", "Purchase Request")
execute:frappe.delete_doc("DocType", "Purchase Request Item")
erpnext.patches.v4_2.recalculate_bom_cost
erpnext.patches.v4_2.fix_gl_entries_for_stock_transactions
erpnext.patches.v4_2.update_requested_and_ordered_qty

View File

@@ -2,6 +2,7 @@ import frappe
from frappe.templates.pages.style_settings import default_properties
def execute():
frappe.reload_doc('website', 'doctype', 'style_settings')
style_settings = frappe.get_doc("Style Settings", "Style Settings")
if not style_settings.apply_style:
style_settings.update(default_properties)

View File

@@ -0,0 +1,52 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import flt
def execute():
from erpnext.utilities.repost_stock import repost
repost(allow_zero_rate=True, only_actual=True)
warehouse_account = frappe.db.sql("""select name, master_name from tabAccount
where ifnull(account_type, '') = 'Warehouse'""")
if warehouse_account:
warehouses = [d[1] for d in warehouse_account]
accounts = [d[0] for d in warehouse_account]
stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where sle.warehouse in (%s)
order by sle.posting_date""" %
', '.join(['%s']*len(warehouses)), tuple(warehouses))
rejected = []
for voucher_type, voucher_no in stock_vouchers:
stock_bal = frappe.db.sql("""select sum(stock_value_difference) from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no =%s and warehouse in (%s)""" %
('%s', '%s', ', '.join(['%s']*len(warehouses))), tuple([voucher_type, voucher_no] + warehouses))
account_bal = frappe.db.sql("""select ifnull(sum(ifnull(debit, 0) - ifnull(credit, 0)), 0)
from `tabGL Entry`
where voucher_type=%s and voucher_no =%s and account in (%s)
group by voucher_type, voucher_no""" %
('%s', '%s', ', '.join(['%s']*len(accounts))), tuple([voucher_type, voucher_no] + accounts))
if stock_bal and account_bal and abs(flt(stock_bal[0][0]) - flt(account_bal[0][0])) > 0.1:
try:
print voucher_type, voucher_no, stock_bal[0][0], account_bal[0][0]
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
voucher = frappe.get_doc(voucher_type, voucher_no)
voucher.make_gl_entries(repost_future_gle=False)
frappe.db.commit()
except Exception, e:
print frappe.get_traceback()
rejected.append([voucher_type, voucher_no])
frappe.db.rollback()
print "Failed to repost: "
print rejected

View File

@@ -0,0 +1,16 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
for d in frappe.db.sql("select name from `tabBOM` where docstatus < 2"):
try:
document = frappe.get_doc('BOM', d[0])
if document.docstatus == 1:
document.ignore_validate_update_after_submit = True
document.calculate_cost()
document.save()
except:
pass

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.db.sql("""update `tabStock Entry` set purpose='Manufacture' where purpose='Manufacture/Repack' and ifnull(production_order,"")!="" """)
frappe.db.sql("""update `tabStock Entry` set purpose='Repack' where purpose='Manufacture/Repack' and ifnull(production_order,"")="" """)

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
from erpnext.utilities.repost_stock import update_bin_qty, get_indented_qty, get_ordered_qty
count=0
for item_code, warehouse in frappe.db.sql("""select distinct item_code, warehouse from
(select item_code, warehouse from tabBin
union
select item_code, warehouse from `tabStock Ledger Entry`) a"""):
try:
count += 1
update_bin_qty(item_code, warehouse, {
"indented_qty": get_indented_qty(item_code, warehouse),
"ordered_qty": get_ordered_qty(item_code, warehouse)
})
if count % 200 == 0:
frappe.db.commit()
except:
frappe.db.rollback()

View File

@@ -1,5 +1,5 @@
{
"allow_import": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-04-03 16:38:41",
"description": "Log of Activities performed by users against Tasks that can be used for tracking time, billing.",
@@ -25,14 +25,6 @@
"read_only": 0,
"reqd": 1
},
{
"fieldname": "hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Hours",
"permlevel": 0,
"read_only": 0
},
{
"fieldname": "to_time",
"fieldtype": "Datetime",
@@ -42,6 +34,14 @@
"read_only": 0,
"reqd": 1
},
{
"fieldname": "hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Hours",
"permlevel": 0,
"read_only": 0
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break",
@@ -151,7 +151,7 @@
"icon": "icon-time",
"idx": 1,
"is_submittable": 1,
"modified": "2014-08-04 05:23:15.740050",
"modified": "2014-10-22 16:53:26.993828",
"modified_by": "Administrator",
"module": "Projects",
"name": "Time Log",

View File

@@ -24,7 +24,7 @@ var get_filters = function(){
{ "value": "Item Group", "label": __("Item Group") },
{ "value": "Supplier", "label": __("Supplier") },
{ "value": "Supplier Type", "label": __("Supplier Type") },
{ "value": "Supplier Type", "label": __("Project") }
{ "value": "Project", "label": __("Project") }
],
"default": "Item"
},

View File

@@ -25,7 +25,7 @@ var get_filters = function(){
{ "value": "Customer", "label": __("Customer") },
{ "value": "Customer Group", "label": __("Customer Group") },
{ "value": "Territory", "label": __("Territory") },
{ "value": "Supplier Type", "label": __("Project") }
{ "value": "Project", "label": __("Project") }
],
"default": "Item"
},

View File

@@ -131,6 +131,7 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({
if(me.is_default("warehouse") ? true : me.warehouse == sl.warehouse) {
var item = me.item_by_name[sl.item_code];
if(item.closing_qty_value==undefined) item.closing_qty_value = 0;
if(me.value_or_qty!="Quantity") {
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
@@ -138,9 +139,20 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({
item.valuation_method : sys_defaults.valuation_method;
var is_fifo = valuation_method == "FIFO";
var diff = me.get_value_diff(wh, sl, is_fifo);
if(sl.voucher_type=="Stock Reconciliation") {
var diff = (sl.qty_after_transaction * sl.valuation_rate) - item.closing_qty_value;
wh.fifo_stack.push([sl.qty_after_transaction, sl.valuation_rate, sl.posting_date]);
wh.balance_qty = sl.qty_after_transaction;
wh.balance_value = sl.valuation_rate * sl.qty_after_transaction;
} else {
var diff = me.get_value_diff(wh, sl, is_fifo);
}
} else {
var diff = sl.qty;
if(sl.voucher_type=="Stock Reconciliation") {
var diff = sl.qty_after_transaction - item.closing_qty_value;
} else {
var diff = sl.qty;
}
}
if(posting_datetime < from_date) {
@@ -150,6 +162,8 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({
} else {
break;
}
item.closing_qty_value += diff;
}
}
},

View File

@@ -9,8 +9,8 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
};
return this.item_warehouse[item][warehouse];
},
get_value_diff: function(wh, sl, is_fifo) {
get_value_diff: function(wh, sl, is_fifo) {
// value
if(sl.qty > 0) {
// incoming - rate is given
@@ -30,9 +30,9 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
} else {
var value_diff = (rate * add_qty);
}
if(add_qty)
wh.fifo_stack.push([add_qty, sl.incoming_rate, sl.posting_date]);
wh.fifo_stack.push([add_qty, sl.incoming_rate, sl.posting_date]);
} else {
// called everytime for maintaining fifo stack
var fifo_value_diff = this.get_fifo_value_diff(wh, sl);
@@ -44,13 +44,13 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
var value_diff = fifo_value_diff;
} else {
// average rate for weighted average
var rate = (wh.balance_qty.toFixed(2) == 0.00 ? 0 :
var rate = (wh.balance_qty.toFixed(2) == 0.00 ? 0 :
flt(wh.balance_value) / flt(wh.balance_qty));
// no change in value if negative qty
if((wh.balance_qty + sl.qty).toFixed(2) >= 0.00)
var value_diff = (rate * sl.qty);
else
else
var value_diff = -wh.balance_value;
}
}
@@ -58,7 +58,6 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
// update balance (only needed in case of valuation)
wh.balance_qty += sl.qty;
wh.balance_value += value_diff;
return value_diff;
},
get_fifo_value_diff: function(wh, sl) {
@@ -66,19 +65,19 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
var fifo_stack = (wh.fifo_stack || []).reverse();
var fifo_value_diff = 0.0;
var qty = -sl.qty;
for(var i=0, j=fifo_stack.length; i<j; i++) {
var batch = fifo_stack.pop();
if(batch[0] >= qty) {
batch[0] = batch[0] - qty;
fifo_value_diff += (qty * batch[1]);
qty = 0.0;
if(batch[0]) {
// batch still has qty put it back
fifo_stack.push(batch);
}
// all qty found
break;
} else {
@@ -87,35 +86,34 @@ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
qty = qty - batch[0];
}
}
// reset the updated stack
wh.fifo_stack = fifo_stack.reverse();
return -fifo_value_diff;
},
get_serialized_value_diff: function(sl) {
var me = this;
var value_diff = 0.0;
$.each(sl.serial_no.trim().split("\n"), function(i, sr) {
if(sr) {
value_diff += flt(me.serialized_buying_rates[sr.trim().toLowerCase()]);
}
});
return value_diff;
},
get_serialized_buying_rates: function() {
var serialized_buying_rates = {};
if (frappe.report_dump.data["Serial No"]) {
$.each(frappe.report_dump.data["Serial No"], function(i, sn) {
serialized_buying_rates[sn.name.toLowerCase()] = flt(sn.incoming_rate);
});
}
return serialized_buying_rates;
},
});
});

View File

@@ -155,7 +155,7 @@ erpnext.TransactionController = erpnext.stock.StockController.extend({
project_name: item.project_name || me.frm.doc.project_name
}
},
callback: function(r) {
if(!r.exc) {
me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
@@ -827,4 +827,35 @@ erpnext.TransactionController = erpnext.stock.StockController.extend({
.appendTo($(this.frm.fields_dict.other_charges_calculation.wrapper).empty());
}
},
is_recurring: function() {
// set default values for recurring documents
if(this.frm.doc.is_recurring) {
var owner_email = this.frm.doc.owner=="Administrator"
? frappe.user_info("Administrator").email
: this.frm.doc.owner;
this.frm.doc.notification_email_address = $.map([cstr(owner_email),
cstr(this.frm.doc.contact_email)], function(v) { return v || null; }).join(", ");
this.frm.doc.repeat_on_day_of_month = frappe.datetime.str_to_obj(this.frm.doc.posting_date).getDate();
}
refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
},
from_date: function() {
// set to_date
if(this.frm.doc.from_date) {
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
'Yearly': 12};
var months = recurring_type_map[this.frm.doc.recurring_type];
if(months) {
var to_date = frappe.datetime.add_months(this.frm.doc.from_date,
months);
this.frm.doc.to_date = frappe.datetime.add_days(to_date, -1);
refresh_field('to_date');
}
}
}
});

View File

@@ -23,7 +23,7 @@ class Quotation(SellingController):
self.validate_order_type()
self.validate_for_items()
self.validate_uom_is_integer("stock_uom", "qty")
self.quotation_to = "Customer" if self.customer else "Lead"
self.validate_quotation_to()
def has_sales_order(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
@@ -54,6 +54,13 @@ class Quotation(SellingController):
if is_sales_item == 'No':
frappe.throw(_("Item {0} must be Sales Item").format(d.item_code))
def validate_quotation_to(self):
if self.customer:
self.quotation_to = "Customer"
self.lead = None
elif self.lead:
self.quotation_to = "Lead"
def update_opportunity(self):
for opportunity in list(set([d.prevdoc_docname for d in self.get("quotation_details")])):
if opportunity:
@@ -139,8 +146,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
return doclist
def _make_customer(source_name, ignore_permissions=False):
quotation = frappe.db.get_value("Quotation", source_name, ["lead", "order_type"])
if quotation and quotation[0]:
quotation = frappe.db.get_value("Quotation", source_name, ["lead", "order_type", "customer"])
if quotation and quotation[0] and not quotation[2]:
lead_name = quotation[0]
customer_name = frappe.db.get_value("Customer", {"lead_name": lead_name},
["name", "customer_name"], as_dict=True)

View File

@@ -195,37 +195,6 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
}
};
cur_frm.cscript.is_recurring = function(doc, dt, dn) {
// set default values for recurring orders
if(doc.is_recurring) {
var owner_email = doc.owner=="Administrator"
? frappe.user_info("Administrator").email
: doc.owner;
doc.notification_email_address = $.map([cstr(owner_email),
cstr(doc.contact_email)], function(v) { return v || null; }).join(", ");
doc.repeat_on_day_of_month = frappe.datetime.str_to_obj(doc.posting_date).getDate();
}
refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
}
cur_frm.cscript.from_date = function(doc, dt, dn) {
// set to_date
if(doc.from_date) {
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
'Yearly': 12};
var months = recurring_type_map[doc.recurring_type];
if(months) {
var to_date = frappe.datetime.add_months(doc.from_date,
months);
doc.to_date = frappe.datetime.add_days(to_date, -1);
refresh_field('to_date');
}
}
}
cur_frm.cscript.send_sms = function() {
frappe.require("assets/erpnext/js/sms_manager.js");
var sms_man = new SMSManager(cur_frm.doc);

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,10 @@
from __future__ import unicode_literals
import frappe
import frappe.utils
from frappe.utils import cstr, flt, getdate, comma_and
from frappe import _
from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document
from erpnext.controllers.selling_controller import SellingController
form_grid_templates = {
@@ -122,8 +118,6 @@ class SalesOrder(SellingController):
if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered'
validate_recurring_document(self)
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
@@ -157,6 +151,8 @@ class SalesOrder(SellingController):
doc.set_status(update=True)
def on_submit(self):
super(SalesOrder, self).on_submit()
self.update_stock_ledger(update_stock = 1)
self.check_credit(self.grand_total)
@@ -165,8 +161,6 @@ class SalesOrder(SellingController):
self.update_prevdoc_status('submit')
frappe.db.set(self, 'status', 'Submitted')
convert_to_recurring(self, "SO/REC/.#####", self.transaction_date)
def on_cancel(self):
# Cannot cancel stopped SO
@@ -255,11 +249,6 @@ class SalesOrder(SellingController):
def get_portal_page(self):
return "order" if self.docstatus==1 else None
def on_update_after_submit(self):
validate_recurring_document(self)
convert_to_recurring(self, "SO/REC/.#####", self.transaction_date)
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
def postprocess(source, doc):

View File

@@ -587,16 +587,18 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
});
frappe.ui.form.on(cur_frm.doctype,"project_name", function(frm) {
frappe.call({
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
args: { project_name: frm.doc.project_name },
callback: function(r, rt) {
if(!r.exc) {
$.each(frm.doc[cur_frm.cscript.fname] || [], function(i, row) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message));
})
if(in_list(["Delivery Note", "Sales Invoice"], frm.doc.doctype)) {
frappe.call({
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
args: { project_name: frm.doc.project_name },
callback: function(r, rt) {
if(!r.exc) {
$.each(frm.doc[cur_frm.cscript.fname] || [], function(i, row) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message));
})
}
}
}
})
})
}
})

View File

@@ -48,11 +48,10 @@ class NamingSeries(Document):
# validate names
for i in options: self.validate_series_name(i)
if self.user_must_always_select:
if options and self.user_must_always_select:
options = [''] + options
default = ''
else:
default = options[0]
default = options[0] if options else ''
# update in property setter
prop_dict = {'options': "\n".join(options), 'default': default}

View File

@@ -71,16 +71,17 @@ def setup_account(args=None):
frappe.db.set_default('desktop:home_page', 'desktop')
website_maker(args.company_name, args.company_tagline, args.name)
website_maker(args.company_name.strip(), args.company_tagline, args.name)
create_logo(args)
frappe.clear_cache()
frappe.db.commit()
except:
traceback = frappe.get_traceback()
for hook in frappe.get_hooks("setup_wizard_exception"):
frappe.get_attr(hook)(traceback, args)
if args:
traceback = frappe.get_traceback()
for hook in frappe.get_hooks("setup_wizard_exception"):
frappe.get_attr(hook)(traceback, args)
raise
@@ -134,7 +135,7 @@ def create_fiscal_year_and_company(args):
frappe.get_doc({
"doctype":"Company",
'domain': args.get("industry"),
'company_name':args.get('company_name'),
'company_name':args.get('company_name').strip(),
'abbr':args.get('company_abbr'),
'default_currency':args.get('currency'),
'country': args.get('country'),
@@ -165,7 +166,7 @@ def set_defaults(args):
global_defaults.update({
'current_fiscal_year': args.curr_fiscal_year,
'default_currency': args.get('currency'),
'default_company':args.get('company_name'),
'default_company':args.get('company_name').strip(),
"country": args.get("country"),
})
@@ -284,7 +285,7 @@ def create_taxes(args):
try:
frappe.get_doc({
"doctype":"Account",
"company": args.get("company_name"),
"company": args.get("company_name").strip(),
"parent_account": _("Duties and Taxes") + " - " + args.get("company_abbr"),
"account_name": args.get("tax_" + str(i)),
"group_or_ledger": "Ledger",
@@ -344,7 +345,7 @@ def create_customers(args):
"customer_type": "Company",
"customer_group": _("Commercial"),
"territory": args.get("country"),
"company": args.get("company_name")
"company": args.get("company_name").strip()
}).insert()
if args.get("customer_contact_" + str(i)):
@@ -364,7 +365,7 @@ def create_suppliers(args):
"doctype":"Supplier",
"supplier_name": supplier,
"supplier_type": _("Local"),
"company": args.get("company_name")
"company": args.get("company_name").strip()
}).insert()
if args.get("supplier_contact_" + str(i)):

View File

@@ -78,7 +78,8 @@ data_map = {
"Stock Ledger Entry": {
"columns": ["name", "posting_date", "posting_time", "item_code", "warehouse",
"actual_qty as qty", "voucher_type", "voucher_no", "project",
"ifnull(incoming_rate,0) as incoming_rate", "stock_uom", "serial_no"],
"ifnull(incoming_rate,0) as incoming_rate", "stock_uom", "serial_no",
"qty_after_transaction", "valuation_rate"],
"order_by": "posting_date, posting_time, name",
"links": {
"item_code": ["Item", "name"],

View File

@@ -3,8 +3,15 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class Batch(Document):
pass
def validate(self):
self.item_has_batch_enabled()
def item_has_batch_enabled(self):
has_batch_no = frappe.db.get_value("Item",self.item,"has_batch_no")
if has_batch_no =='No':
frappe.throw(_("The selected item cannot have Batch"))

View File

@@ -0,0 +1,14 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.exceptions import ValidationError
import unittest
class TestBatch(unittest.TestCase):
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
"name": "_test Batch",
"item": "_Test Item"
}).save)

View File

@@ -11,27 +11,27 @@ class Bin(Document):
def validate(self):
if self.get("__islocal") or not self.stock_uom:
self.stock_uom = frappe.db.get_value('Item', self.item_code, 'stock_uom')
self.validate_mandatory()
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
def validate_mandatory(self):
qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty']
for f in qf:
if (not getattr(self, f, None)) or (not self.get(f)):
if (not getattr(self, f, None)) or (not self.get(f)):
self.set(f, 0.0)
def update_stock(self, args):
self.update_qty(args)
if args.get("actual_qty"):
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after
if not args.get("posting_date"):
args["posting_date"] = nowdate()
# update valuation and qty after transaction for post dated entry
update_entries_after({
"item_code": self.item_code,
@@ -39,21 +39,34 @@ class Bin(Document):
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time")
})
def update_qty(self, args):
# update the stock values (for current quantities)
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
if args.get("voucher_type")=="Stock Reconciliation":
if args.get('is_cancelled') == 'No':
self.actual_qty = args.get("qty_after_transaction")
else:
qty_after_transaction = frappe.db.get_value("""select qty_after_transaction
from `tabStock Ledger Entry`
where item_code=%s and warehouse=%s
and not (voucher_type='Stock Reconciliation' and voucher_no=%s)
order by posting_date desc limit 1""",
(self.item_code, self.warehouse, args.get('voucher_no')))
self.actual_qty = flt(qty_after_transaction[0][0]) if qty_after_transaction else 0.0
else:
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
self.save()
def get_first_sle(self):
sle = frappe.db.sql("""
select * from `tabStock Ledger Entry`
@@ -62,4 +75,4 @@ class Bin(Document):
order by timestamp(posting_date, posting_time) asc, name asc
limit 1
""", (self.item_code, self.warehouse), as_dict=1)
return sle and sle[0] or None
return sle and sle[0] or None

View File

@@ -52,11 +52,15 @@ class DeliveryNote(SellingController):
else:
df.delete_key("__print_hide")
toggle_print_hide(self.meta, "currency")
item_meta = frappe.get_meta("Delivery Note Item")
for fieldname in ("rate", "amount", "price_list_rate", "discount_percentage"):
toggle_print_hide(item_meta, fieldname)
print_hide_fields = {
"parent": ["grand_total_export", "rounded_total_export", "in_words_export", "currency", "net_total_export"],
"items": ["rate", "amount", "price_list_rate", "discount_percentage"]
}
for key, fieldname in print_hide_fields.items():
for f in fieldname:
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
def get_portal_page(self):
return "shipment" if self.docstatus==1 else None
@@ -245,7 +249,7 @@ class DeliveryNote(SellingController):
sl_entries = []
for d in self.get_item_list():
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \
and d.warehouse:
and d.warehouse and flt(d['qty']):
self.update_reserved_qty(d)
sl_entries.append(self.get_sl_entries(d, {

View File

@@ -19,7 +19,7 @@ cur_frm.cscript.refresh = function(doc) {
cur_frm.cscript.edit_prices_button();
if (!doc.__islocal && doc.is_stock_item == 'Yes') {
cur_frm.toggle_enable(['has_serial_no', 'is_stock_item', 'valuation_method'],
cur_frm.toggle_enable(['has_serial_no', 'is_stock_item', 'valuation_method', 'has_batch_no'],
(doc.__onload && doc.__onload.sle_exists=="exists") ? false : true);
}
@@ -185,4 +185,4 @@ cur_frm.cscript.image = function() {
else {
msgprint(__("You may need to update: {0}", [frappe.meta.get_docfield(cur_frm.doc.doctype, "description_html").label]));
}
}
}

View File

@@ -187,13 +187,14 @@ class Item(WebsiteGenerator):
def cant_change(self):
if not self.get("__islocal"):
vals = frappe.db.get_value("Item", self.name,
["has_serial_no", "is_stock_item", "valuation_method"], as_dict=True)
["has_serial_no", "is_stock_item", "valuation_method", "has_batch_no"], as_dict=True)
if vals and ((self.is_stock_item == "No" and vals.is_stock_item == "Yes") or
vals.has_serial_no != self.has_serial_no or
vals.has_batch_no != self.has_batch_no or
cstr(vals.valuation_method) != cstr(self.valuation_method)):
if self.check_if_sle_exists() == "exists":
frappe.throw(_("As there are existing stock transactions for this item, you can not change the values of 'Has Serial No', 'Is Stock Item' and 'Valuation Method'"))
frappe.throw(_("As there are existing stock transactions for this item, you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
def validate_item_type_for_reorder(self):
if self.re_order_level or len(self.get("item_reorder", {"material_request_type": "Purchase"})):

View File

@@ -1,5 +1,5 @@
frappe.listview_settings['Item'] = {
add_fields: ["item_name", "stock_uom", "item_group", "image",
"is_stock_item", "is_sales_item", "is_purchase_item",
"is_manufactured_item", "show_in_website"]
add_fields: ["`tabItem`.`item_name`", "`tabItem`.`stock_uom`", "`tabItem`.`item_group`", "`tabItem`.`image`",
"`tabItem`.`is_stock_item`", "`tabItem`.`is_sales_item`", "`tabItem`.`is_purchase_item`",
"`tabItem`.`is_manufactured_item`", "`tabItem`.`show_in_website`"]
};

View File

@@ -9,6 +9,6 @@ class TestItem(unittest.TestCase):
def test_duplicate_item(self):
from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.insert)
self.assertRaises(ItemPriceDuplicateItem, doc.save)
test_records = frappe.get_test_records('Item Price')

View File

@@ -16,5 +16,11 @@
"item_code": "_Test Item 2",
"price_list": "_Test Price List Rest of the World",
"price_list_rate": 20
},
{
"doctype": "Item Price",
"item_code": "_Test Item Home Desktop 100",
"price_list": "_Test Price List",
"price_list_rate": 1000
}
]

View File

@@ -15,7 +15,7 @@ class LandedCostVoucher(Document):
self.set("landed_cost_items", [])
for pr in self.get("landed_cost_purchase_receipts"):
pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description,
pr_item.qty, pr_item.rate, pr_item.amount, pr_item.name
pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name
from `tabPurchase Receipt Item` pr_item where parent = %s
and exists(select name from tabItem where name = pr_item.item_code and is_stock_item = 'Yes')""",
pr.purchase_receipt, as_dict=True)
@@ -25,8 +25,8 @@ class LandedCostVoucher(Document):
item.item_code = d.item_code
item.description = d.description
item.qty = d.qty
item.rate = d.rate
item.amount = d.amount
item.rate = d.base_rate
item.amount = d.base_amount
item.purchase_receipt = pr.purchase_receipt
item.purchase_receipt_item = d.name
@@ -97,10 +97,10 @@ class LandedCostVoucher(Document):
# update stock & gl entries for cancelled state of PR
pr.docstatus = 2
pr.update_stock()
pr.update_stock_ledger()
pr.make_gl_entries_on_cancel()
# update stock & gl entries for submit state of PR
pr.docstatus = 1
pr.update_stock()
pr.update_stock_ledger()
pr.make_gl_entries()

View File

@@ -79,30 +79,9 @@ class MaterialRequest(BuyingController):
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
# Though the creation of Material Request from a Production Plan can be rethought to fix this
def update_bin(self, is_submit, is_stopped):
""" Update Quantity Requested for Purchase in Bin for Material Request of type 'Purchase'"""
from erpnext.stock.utils import update_bin
for d in self.get('indent_details'):
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes":
if not d.warehouse:
frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code))
qty =flt(d.qty)
if is_stopped:
qty = (d.qty > d.ordered_qty) and flt(flt(d.qty) - flt(d.ordered_qty)) or 0
args = {
"item_code": d.item_code,
"warehouse": d.warehouse,
"indented_qty": (is_submit and 1 or -1) * flt(qty),
"posting_date": self.transaction_date
}
update_bin(args)
def on_submit(self):
frappe.db.set(self, 'status', 'Submitted')
self.update_bin(is_submit = 1, is_stopped = 0)
self.update_requested_qty()
def check_modified_date(self):
mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""",
@@ -115,23 +94,18 @@ class MaterialRequest(BuyingController):
def update_status(self, status):
self.check_modified_date()
self.update_bin(is_submit = (status == 'Submitted') and 1 or 0, is_stopped = 1)
self.update_requested_qty()
frappe.db.set(self, 'status', cstr(status))
frappe.msgprint(_("Status updated to {0}").format(_(status)))
def on_cancel(self):
# Step 1:=> Get Purchase Common Obj
pc_obj = frappe.get_doc('Purchase Common')
# Step 2:=> Check for stopped status
pc_obj.check_for_stopped_status(self.doctype, self.name)
# Step 3:=> Check if Purchase Order has been submitted against current Material Request
pc_obj.check_docstatus(check = 'Next', doctype = 'Purchase Order', docname = self.name, detail_doctype = 'Purchase Order Item')
# Step 4:=> Update Bin
self.update_bin(is_submit = 0, is_stopped = (cstr(self.status) == 'Stopped') and 1 or 0)
# Step 5:=> Set Status
self.update_requested_qty()
frappe.db.set(self,'status','Cancelled')
def update_completed_qty(self, mr_items=None):
@@ -162,56 +136,47 @@ class MaterialRequest(BuyingController):
self.per_ordered = flt((per_ordered / flt(len(item_doclist))) * 100.0, 2)
frappe.db.set_value(self.doctype, self.name, "per_ordered", self.per_ordered)
def update_completed_qty(doc, method):
if doc.doctype == "Stock Entry":
def update_requested_qty(self, mr_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
from erpnext.stock.utils import get_bin
def _update_requested_qty(item_code, warehouse):
requested_qty = frappe.db.sql("""select sum(mr_item.qty - ifnull(mr_item.ordered_qty, 0))
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
where mr_item.item_code=%s and mr_item.warehouse=%s
and mr_item.qty > ifnull(mr_item.ordered_qty, 0) and mr_item.parent=mr.name
and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
bin_doc = get_bin(item_code, warehouse)
bin_doc.indented_qty = flt(requested_qty[0][0]) if requested_qty else 0
bin_doc.save()
item_wh_list = []
for d in self.get("indent_details"):
if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \
and frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse:
item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list:
_update_requested_qty(item_code, warehouse)
def update_completed_and_requested_qty(stock_entry, method):
if stock_entry.doctype == "Stock Entry":
material_request_map = {}
for d in doc.get("mtn_details"):
for d in stock_entry.get("mtn_details"):
if d.material_request:
material_request_map.setdefault(d.material_request, []).append(d.material_request_item)
for mr_name, mr_items in material_request_map.items():
mr_obj = frappe.get_doc("Material Request", mr_name)
for mr, mr_item_rows in material_request_map.items():
if mr and mr_item_rows:
mr_obj = frappe.get_doc("Material Request", mr)
if mr_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(_("Material Request {0} is cancelled or stopped").format(mr_obj.name),
frappe.InvalidStatusError)
if mr_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError)
_update_requested_qty(doc, mr_obj, mr_items)
# update ordered percentage and qty
mr_obj.update_completed_qty(mr_items)
def _update_requested_qty(doc, mr_obj, mr_items):
"""update requested qty (before ordered_qty is updated)"""
from erpnext.stock.utils import update_bin
for mr_item_name in mr_items:
mr_item = mr_obj.get("indent_details", {"name": mr_item_name})
se_detail = doc.get("mtn_details", {"material_request": mr_obj.name,
"material_request_item": mr_item_name})
if mr_item and se_detail:
mr_item = mr_item[0]
se_detail = se_detail[0]
mr_item.ordered_qty = flt(mr_item.ordered_qty)
mr_item.qty = flt(mr_item.qty)
se_detail.transfer_qty = flt(se_detail.transfer_qty)
if se_detail.docstatus == 2 and mr_item.ordered_qty > mr_item.qty \
and se_detail.transfer_qty == mr_item.ordered_qty:
add_indented_qty = mr_item.qty
elif se_detail.docstatus == 1 and \
mr_item.ordered_qty + se_detail.transfer_qty > mr_item.qty:
add_indented_qty = mr_item.qty - mr_item.ordered_qty
else:
add_indented_qty = se_detail.transfer_qty
update_bin({
"item_code": se_detail.item_code,
"warehouse": se_detail.t_warehouse,
"indented_qty": (se_detail.docstatus==2 and 1 or -1) * add_indented_qty,
"posting_date": doc.posting_date,
})
mr_obj.update_completed_qty(mr_item_rows)
mr_obj.update_requested_qty(mr_item_rows)
def set_missing_values(source, target_doc):
target_doc.run_method("set_missing_values")

View File

@@ -58,12 +58,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(se.doctype, "Stock Entry")
self.assertEquals(len(se.get("mtn_details")), len(mr.get("indent_details")))
def _test_requested_qty(self, qty1, qty2):
self.assertEqual(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100",
"warehouse": "_Test Warehouse - _TC"}, "indented_qty")), qty1)
self.assertEqual(flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 200",
"warehouse": "_Test Warehouse - _TC"}, "indented_qty")), qty2)
def _insert_stock_entry(self, qty1, qty2):
se = frappe.get_doc({
"company": "_Test Company",
@@ -103,7 +97,8 @@ class TestMaterialRequest(unittest.TestCase):
se.submit()
def test_completed_qty_for_purchase(self):
frappe.db.sql("""delete from `tabBin`""")
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -115,8 +110,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 0)
self._test_requested_qty(54.0, 3.0)
# map a purchase order
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
po_doc = make_purchase_order(mr.name)
@@ -149,7 +142,12 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.per_ordered, 50)
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 27.0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 1.5)
self._test_requested_qty(27.0, 1.5)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
po.cancel()
# check if per complete is as expected
@@ -158,11 +156,15 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, None)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, None)
self._test_requested_qty(54.0, 3.0)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_transfer(self):
frappe.db.sql("""delete from `tabBin`""")
frappe.db.sql("""delete from `tabStock Ledger Entry`""")
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -175,7 +177,11 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 0)
self._test_requested_qty(54.0, 3.0)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
@@ -226,7 +232,11 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 27.0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 1.5)
self._test_requested_qty(27.0, 1.5)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
# check if per complete is as expected for Stock Entry cancelled
se.cancel()
@@ -235,11 +245,15 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 0)
self._test_requested_qty(54.0, 3.0)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_over_transfer(self):
frappe.db.sql("""delete from `tabBin`""")
frappe.db.sql("""delete from `tabStock Ledger Entry`""")
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -252,8 +266,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 0)
self._test_requested_qty(54.0, 3.0)
# map a stock entry
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
@@ -297,7 +309,12 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.per_ordered, 100)
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 60.0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 3.0)
self._test_requested_qty(0.0, 0.0)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2)
# check if per complete is as expected for Stock Entry cancelled
se.cancel()
@@ -306,7 +323,11 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEquals(mr.get("indent_details")[0].ordered_qty, 0)
self.assertEquals(mr.get("indent_details")[1].ordered_qty, 0)
self._test_requested_qty(54.0, 3.0)
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEquals(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEquals(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_incorrect_mapping_of_stock_entry(self):
# submit material request of type Purchase
@@ -348,5 +369,9 @@ class TestMaterialRequest(unittest.TestCase):
mr.company = "_Test Company 1"
self.assertRaises(InvalidWarehouseCompany, mr.insert)
def _get_requested_qty(self, item_code, warehouse):
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty"))
test_dependencies = ["Currency Exchange"]
test_records = frappe.get_test_records('Material Request')

View File

@@ -162,8 +162,7 @@ def item_details(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
return frappe.db.sql("""select name, item_name, description from `tabItem`
where name in ( select item_code FROM `tabDelivery Note Item`
where parent= %s
and ifnull(qty, 0) > ifnull(packed_qty, 0))
where parent= %s)
and %s like "%s" %s
limit %s, %s """ % ("%s", searchfield, "%s",
get_match_cond(doctype), "%s", "%s"),

View File

@@ -8,7 +8,6 @@ from frappe.utils import cstr, flt, cint
from frappe import _
import frappe.defaults
from erpnext.stock.utils import update_bin
from erpnext.controllers.buying_controller import BuyingController
@@ -130,7 +129,7 @@ class PurchaseReceipt(BuyingController):
if not d.prevdoc_docname:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
def update_stock(self):
def update_stock_ledger(self):
sl_entries = []
stock_items = self.get_stock_items()
@@ -157,29 +156,19 @@ class PurchaseReceipt(BuyingController):
self.make_sl_entries(sl_entries)
def update_ordered_qty(self):
stock_items = self.get_stock_items()
po_map = {}
for d in self.get("purchase_receipt_details"):
if d.item_code in stock_items and d.warehouse \
and cstr(d.prevdoc_doctype) == 'Purchase Order':
if d.prevdoc_doctype and d.prevdoc_doctype == "Purchase Order" and d.prevdoc_detail_docname:
po_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname)
already_received_qty = self.get_already_received_qty(d.prevdoc_docname,
d.prevdoc_detail_docname)
po_qty, ordered_warehouse = self.get_po_qty_and_warehouse(d.prevdoc_detail_docname)
for po, po_item_rows in po_map.items():
if po and po_item_rows:
po_obj = frappe.get_doc("Purchase Order", po)
if not ordered_warehouse:
frappe.throw(_("Warehouse is missing in Purchase Order"))
if po_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(_("Material Request {0} is cancelled or stopped").format(po), frappe.InvalidStatusError)
if already_received_qty + d.qty > po_qty:
ordered_qty = - (po_qty - already_received_qty) * flt(d.conversion_factor)
else:
ordered_qty = - flt(d.qty) * flt(d.conversion_factor)
update_bin({
"item_code": d.item_code,
"warehouse": ordered_warehouse,
"posting_date": self.posting_date,
"ordered_qty": flt(ordered_qty) if self.docstatus==1 else -flt(ordered_qty)
})
po_obj.update_ordered_qty(po_item_rows)
def get_already_received_qty(self, po, po_detail):
qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item`
@@ -234,7 +223,7 @@ class PurchaseReceipt(BuyingController):
self.update_ordered_qty()
self.update_stock()
self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "purchase_receipt_details")
@@ -265,11 +254,13 @@ class PurchaseReceipt(BuyingController):
frappe.db.set(self,'status','Cancelled')
self.update_ordered_qty()
self.update_stock()
self.update_stock_ledger()
self.update_prevdoc_status()
# Must be called after updating received qty in PO
self.update_ordered_qty()
pc_obj.update_last_purchase_rate(self, 0)
self.make_gl_entries_on_cancel()

View File

@@ -95,7 +95,7 @@ class TestPurchaseReceipt(unittest.TestCase):
pr.insert()
self.assertEquals(len(pr.get("pr_raw_material_details")), 2)
self.assertEquals(pr.get("purchase_receipt_details")[0].rm_supp_cost, 70000.0)
self.assertEquals(pr.get("purchase_receipt_details")[0].rm_supp_cost, 20750.0)
def test_serial_no_supplier(self):
@@ -151,6 +151,6 @@ def set_perpetual_inventory(enable=1):
accounts_settings.save()
test_dependencies = ["BOM"]
test_dependencies = ["BOM", "Item Price"]
test_records = frappe.get_test_records('Purchase Receipt')

View File

@@ -120,7 +120,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
clean_up: function() {
// Clear Production Order record from locals, because it is updated via Stock Entry
if(this.frm.doc.production_order &&
this.frm.doc.purpose == "Manufacture/Repack") {
this.frm.doc.purpose == "Manufacture") {
frappe.model.remove_from_locals("Production Order",
this.frm.doc.production_order);
}
@@ -162,7 +162,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
},
toggle_enable_bom: function() {
this.frm.toggle_enable("bom_no", !this.frm.doc.production_order);
this.frm.toggle_enable("bom_no", this.frm.doc.purpose!="Manufacture");
},
get_doctype_docname: function() {
@@ -339,6 +339,8 @@ cur_frm.cscript.toggle_related_fields = function(doc) {
cur_frm.fields_dict["mtn_details"].grid.set_column_disp("s_warehouse", !disable_from_warehouse);
cur_frm.fields_dict["mtn_details"].grid.set_column_disp("t_warehouse", !disable_to_warehouse);
cur_frm.cscript.toggle_enable_bom();
if(doc.purpose == 'Purchase Return') {
doc.customer = doc.customer_name = doc.customer_address =
doc.delivery_note_no = doc.sales_invoice_no = null;
@@ -351,6 +353,8 @@ cur_frm.cscript.toggle_related_fields = function(doc) {
doc.delivery_note_no = doc.sales_invoice_no = doc.supplier =
doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no = null;
}
}
cur_frm.fields_dict['production_order'].get_query = function(doc) {
@@ -457,4 +461,5 @@ cur_frm.fields_dict.customer.get_query = function(doc, cdt, cdn) {
cur_frm.fields_dict.supplier.get_query = function(doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.supplier_query" }
}
cur_frm.add_fetch('production_order', 'total_fixed_cost', 'total_fixed_cost');
cur_frm.add_fetch('production_order', 'total_fixed_cost', 'total_fixed_cost');
cur_frm.add_fetch('bom_no', 'total_fixed_cost', 'total_fixed_cost');

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ class StockEntry(StockController):
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "transfer_qty")
self.validate_warehouse(pro_obj)
self.validate_production_order(pro_obj)
self.validate_production_order()
self.get_stock_and_rate()
self.validate_incoming_rate()
self.validate_bom()
@@ -54,6 +54,7 @@ class StockEntry(StockController):
self.validate_valuation_rate()
self.set_total_amount()
def on_submit(self):
self.update_stock_ledger()
@@ -74,7 +75,7 @@ class StockEntry(StockController):
def validate_purpose(self):
valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer",
"Manufacture/Repack", "Subcontract", "Sales Return", "Purchase Return"]
"Manufacture", "Repack", "Subcontract", "Sales Return", "Purchase Return"]
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
@@ -137,7 +138,7 @@ class StockEntry(StockController):
if self.purpose in target_mandatory and not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture/Repack":
if self.purpose in ["Manufacture", "Repack"]:
if validate_for_manufacture_repack:
if d.bom_no:
d.s_warehouse = None
@@ -156,14 +157,11 @@ class StockEntry(StockController):
if cstr(d.s_warehouse) == cstr(d.t_warehouse):
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
def validate_production_order(self, pro_obj=None):
if not pro_obj:
if self.production_order:
pro_obj = frappe.get_doc('Production Order', self.production_order)
else:
return
if self.purpose == "Manufacture/Repack":
def validate_production_order(self):
if self.purpose == "Manufacture":
# check if production order is entered
if not self.production_order:
frappe.throw(_("Production order number is mandatory for stock entry purpose manufacture"))
# check for double entry
self.check_duplicate_entry_for_production_order()
elif self.purpose != "Material Transfer":
@@ -192,7 +190,7 @@ class StockEntry(StockController):
+ self.production_order + ":" + ", ".join(other_ste), DuplicateEntryForProductionOrderError)
def validate_valuation_rate(self):
if self.purpose == "Manufacture/Repack":
if self.purpose in ["Manufacture", "Repack"]:
valuation_at_source, valuation_at_target = 0, 0
for d in self.get("mtn_details"):
if d.s_warehouse and not d.t_warehouse:
@@ -248,7 +246,7 @@ class StockEntry(StockController):
raw_material_cost += flt(d.amount)
# set incoming rate for fg item
if self.purpose == "Manufacture/Repack":
if self.purpose in ["Manufacture", "Repack"]:
number_of_fg_items = len([t.t_warehouse for t in self.get("mtn_details") if t.t_warehouse])
for d in self.get("mtn_details"):
if d.bom_no or (d.t_warehouse and number_of_fg_items == 1):
@@ -391,7 +389,7 @@ class StockEntry(StockController):
pro_doc = frappe.get_doc("Production Order", self.production_order)
_validate_production_order(pro_doc)
pro_doc.run_method("update_status")
if self.purpose == "Manufacture/Repack":
if self.purpose == "Manufacture":
pro_doc.run_method("update_produced_qty")
self.update_planned_qty(pro_doc)
@@ -463,20 +461,20 @@ class StockEntry(StockController):
def get_items(self):
self.set('mtn_details', [])
self.validate_production_order()
pro_obj = None
if self.production_order:
# common validations
pro_obj = frappe.get_doc('Production Order', self.production_order)
if pro_obj:
self.validate_production_order(pro_obj)
self.bom_no = pro_obj.bom_no
else:
# invalid production order
self.production_order = None
if self.bom_no:
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture/Repack",
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Subcontract"]:
if self.production_order and self.purpose == "Material Transfer":
item_dict = self.get_pending_raw_materials(pro_obj)
@@ -493,7 +491,7 @@ class StockEntry(StockController):
self.add_to_stock_entry_detail(item_dict)
# add finished good item to Stock Entry Detail table -- along with bom_no
if self.production_order and self.purpose == "Manufacture/Repack":
if self.production_order and self.purpose == "Manufacture":
item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name",
"description", "stock_uom", "expense_account", "buying_cost_center"], as_dict=1)
self.add_to_stock_entry_detail({
@@ -509,7 +507,7 @@ class StockEntry(StockController):
}
}, bom_no=pro_obj.bom_no)
elif self.purpose in ["Material Receipt", "Manufacture/Repack"]:
elif self.purpose in ["Material Receipt", "Repack"]:
if self.purpose=="Material Receipt":
self.from_warehouse = ""

View File

@@ -6,7 +6,8 @@
"Material Issue": "icon-arrow-right",
"Material Receipt": "icon-arrow-left",
"Material Transfer": "icon-resize-horizontal",
"Manufacture/Repack": "icon-wrench",
"Manufacture": "icon-wrench",
"Repack": "icon-wrench",
"Sales Return": "icon-warning-sign",
"Purchase Return": "icon-warning-sign",
"Subcontract": "icon-truck"

View File

@@ -108,6 +108,6 @@
],
"posting_date": "2013-01-25",
"posting_time": "17:14:24",
"purpose": "Manufacture/Repack"
"purpose": "Repack"
}
]

View File

@@ -9,14 +9,64 @@ from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
def get_sle(**args):
condition, values = "", []
for key, value in args.iteritems():
condition += " and " if condition else " where "
condition += "`{0}`=%s".format(key)
values.append(value)
return frappe.db.sql("""select * from `tabStock Ledger Entry` %s
order by timestamp(posting_date, posting_time) desc, name desc limit 1"""% condition,
values, as_dict=1)
def make_zero(item_code, warehouse):
sle = get_sle(item_code = item_code, warehouse = warehouse)
qty = sle[0].qty_after_transaction if sle else 0
if qty < 0:
make_stock_entry(item_code, None, warehouse, abs(qty), incoming_rate=10)
elif qty > 0:
make_stock_entry(item_code, warehouse, None, qty, incoming_rate=10)
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
set_perpetual_inventory(0)
if hasattr(self, "old_default_company"):
frappe.db.set_default("company", self.old_default_company)
def test_fifo(self):
frappe.db.set_default("allow_negative_stock", 1)
item_code = "_Test Item 2"
warehouse = "_Test Warehouse - _TC"
make_zero(item_code, warehouse)
make_stock_entry(item_code, None, warehouse, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
# negative qty
make_zero(item_code, warehouse)
make_stock_entry(item_code, warehouse, None, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-1, 10]], eval(sle.stock_queue))
# further negative
make_stock_entry(item_code, warehouse, None, 1)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-2, 10]], eval(sle.stock_queue))
# move stock to positive
make_stock_entry(item_code, None, warehouse, 3, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
frappe.db.set_default("allow_negative_stock", 0)
def test_auto_material_request(self):
frappe.db.sql("""delete from `tabMaterial Request Item`""")
frappe.db.sql("""delete from `tabMaterial Request`""")
@@ -821,19 +871,19 @@ class TestStockEntry(unittest.TestCase):
se = frappe.copy_doc(test_records[0]).insert()
self.assertRaises (StockFreezeError, se.submit)
frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", 0)
def test_production_order(self):
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
"is_default": 1, "docstatus": 1})
production_order = frappe.new_doc("Production Order")
production_order.update({
"company": "_Test Company",
"fg_warehouse": "_Test Warehouse 1 - _TC",
"production_item": "_Test FG Item 2",
"fg_warehouse": "_Test Warehouse 1 - _TC",
"production_item": "_Test FG Item 2",
"bom_no": bom_no,
"qty": 1.0,
"stock_uom": "Nos",
"stock_uom": "Nos",
"wip_warehouse": "_Test Warehouse - _TC"
})
production_order.insert()
@@ -843,7 +893,7 @@ class TestStockEntry(unittest.TestCase):
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.update({
"purpose": "Manufacture/Repack",
"purpose": "Manufacture",
"production_order": production_order.name,
"bom_no": bom_no,
"fg_completed_qty": "1",

View File

@@ -44,11 +44,14 @@ class StockLedgerEntry(Document):
formatdate(self.posting_date), self.posting_time))
def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','actual_qty','company']
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory"))
def validate_item(self):
item_det = frappe.db.sql("""select name, has_batch_no, docstatus, is_stock_item
from tabItem where name=%s""", self.item_code, as_dict=True)[0]

View File

@@ -1,5 +1,5 @@
{
"allow_copy": 1,
"allow_copy": 1,
"autoname": "SR/.######",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@@ -7,6 +7,7 @@
"doctype": "DocType",
"fields": [
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_filter": 0,
@@ -118,7 +119,7 @@
"idx": 1,
"is_submittable": 1,
"max_attachments": 1,
"modified": "2014-05-26 03:05:54.024413",
"modified": "2014-10-07 12:43:52.825575",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",

View File

@@ -16,13 +16,11 @@ class StockReconciliation(StockController):
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
def validate(self):
self.entries = []
self.validate_data()
self.validate_expense_account()
def on_submit(self):
self.insert_stock_ledger_entries()
self.update_stock_ledger()
self.make_gl_entries()
def on_cancel(self):
@@ -126,10 +124,9 @@ class StockReconciliation(StockController):
except Exception, e:
self.validation_messages.append(_("Row # ") + ("%d: " % (row_num)) + cstr(e))
def insert_stock_ledger_entries(self):
def update_stock_ledger(self):
""" find difference between current and expected entries
and create stock ledger entries based on the difference"""
from erpnext.stock.utils import get_valuation_method
from erpnext.stock.stock_ledger import get_previous_sle
row_template = ["item_code", "warehouse", "qty", "valuation_rate"]
@@ -141,105 +138,27 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(data[data.index(self.head_row)+1:]):
row = frappe._dict(zip(row_template, row))
row["row_num"] = row_num
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
# check valuation rate mandatory
if row.qty not in ["", None] and not row.valuation_rate and \
flt(previous_sle.get("qty_after_transaction")) <= 0:
frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code))
if row.qty in ("", None) or row.valuation_rate in ("", None):
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
change_in_qty = row.qty not in ["", None] and \
(flt(row.qty) - flt(previous_sle.get("qty_after_transaction")))
if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction")
change_in_rate = row.valuation_rate not in ["", None] and \
(flt(row.valuation_rate) - flt(previous_sle.get("valuation_rate")))
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate")
if get_valuation_method(row.item_code) == "Moving Average":
self.sle_for_moving_avg(row, previous_sle, change_in_qty, change_in_rate)
# if row.qty and not row.valuation_rate:
# frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code))
else:
self.sle_for_fifo(row, previous_sle, change_in_qty, change_in_rate)
self.insert_entries(row)
def sle_for_moving_avg(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for Moving Average valuation"""
def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate):
if previous_valuation_rate == 0:
return flt(valuation_rate)
else:
if valuation_rate in ["", None]:
valuation_rate = previous_valuation_rate
return (qty * valuation_rate - previous_qty * previous_valuation_rate) \
/ flt(qty - previous_qty)
if change_in_qty:
# if change in qty, irrespective of change in rate
incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")), flt(previous_sle.get("valuation_rate")))
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": change_in_qty, "incoming_rate": incoming_rate}, row)
elif change_in_rate and flt(previous_sle.get("qty_after_transaction")) > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
incoming_rate = _get_incoming_rate(
flt(previous_sle.get("qty_after_transaction"))+1, flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")),
flt(previous_sle.get("valuation_rate")))
# +1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment +1"
self.insert_entries({"actual_qty": 1, "incoming_rate": incoming_rate}, row)
# -1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment -1"
self.insert_entries({"actual_qty": -1}, row)
def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for FIFO valuation"""
previous_stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
previous_stock_qty = sum((batch[0] for batch in previous_stock_queue))
previous_stock_value = sum((batch[0] * batch[1] for batch in \
previous_stock_queue))
def _insert_entries():
if previous_stock_queue != [[row.qty, row.valuation_rate]]:
# make entry as per attachment
if flt(row.qty):
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": row.qty,
"incoming_rate": flt(row.valuation_rate)}, row)
# Make reverse entry
if previous_stock_qty:
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Reverse Entry"
self.insert_entries({"actual_qty": -1 * previous_stock_qty,
"incoming_rate": previous_stock_qty < 0 and
flt(row.valuation_rate) or 0}, row)
if change_in_qty:
if row.valuation_rate in ["", None]:
# dont want change in valuation
if previous_stock_qty > 0:
# set valuation_rate as previous valuation_rate
row.valuation_rate = previous_stock_value / flt(previous_stock_qty)
_insert_entries()
elif change_in_rate and previous_stock_qty > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
row.qty = previous_stock_qty
_insert_entries()
def insert_entries(self, opts, row):
def insert_entries(self, row):
"""Insert Stock Ledger Entries"""
args = frappe._dict({
"doctype": "Stock Ledger Entry",
@@ -251,16 +170,13 @@ class StockReconciliation(StockController):
"voucher_no": self.name,
"company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"voucher_detail_no": row.voucher_detail_no,
"fiscal_year": self.fiscal_year,
"is_cancelled": "No"
"is_cancelled": "No",
"qty_after_transaction": row.qty,
"valuation_rate": row.valuation_rate
})
args.update(opts)
self.make_sl_entries([args])
# append to entries
self.entries.append(args)
def delete_and_repost_sle(self):
""" Delete Stock Ledger Entries related to this voucher
and repost future Stock Ledger Entries"""
@@ -295,7 +211,7 @@ class StockReconciliation(StockController):
if not self.expense_account:
msgprint(_("Please enter Expense Account"), raise_exception=1)
elif not frappe.db.sql("""select * from `tabStock Ledger Entry`"""):
elif not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry"))

View File

@@ -28,7 +28,7 @@ class TestStockReconciliation(unittest.TestCase):
[20, "", "2012-12-26", "12:05", 16000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[0, "", "2012-12-26", "12:10", 0, -5, -6000]
]
for d in input_data:
@@ -63,16 +63,16 @@ class TestStockReconciliation(unittest.TestCase):
input_data = [
[50, 1000, "2012-12-26", "12:00", 50000, 45, 48000],
[5, 1000, "2012-12-26", "12:00", 5000, 0, 0],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 12000],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 11500],
[25, 900, "2012-12-26", "12:00", 22500, 20, 22500],
[20, 500, "2012-12-26", "12:00", 10000, 15, 18000],
[50, 1000, "2013-01-01", "12:00", 50000, 65, 68000],
[5, 1000, "2013-01-01", "12:00", 5000, 20, 23000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 12000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 11500],
[20, "", "2012-12-26", "12:05", 18000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[10, 2000, "2012-12-26", "12:10", 20000, 5, 7600],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 12512.73],
[0, "", "2012-12-26", "12:10", 0, -5, -5142.86]
]

View File

@@ -6,18 +6,17 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.model.document import Document
class StockSettings(Document):
def validate(self):
for key in ["item_naming_by", "item_group", "stock_uom",
"allow_negative_stock"]:
for key in ["item_naming_by", "item_group", "stock_uom", "allow_negative_stock"]:
frappe.db.set_default(key, self.get(key, ""))
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
set_by_naming_series("Item", "item_code",
set_by_naming_series("Item", "item_code",
self.get("item_naming_by")=="Naming Series", hide_name_field=True)
stock_frozen_limit = 356
@@ -25,3 +24,5 @@ class StockSettings(Document):
if submitted_stock_frozen > stock_frozen_limit:
self.stock_frozen_upto_days = stock_frozen_limit
frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit)

View File

@@ -1 +0,0 @@
Average "age" of an Item in a particular Warehouse based on First-in-first-out (FIFO).

View File

@@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@@ -1,183 +0,0 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.pages['stock-ageing'].onload = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Stock Ageing'),
single_column: true
});
new erpnext.StockAgeing(wrapper);
wrapper.appframe.add_module_icon("Stock")
}
frappe.require("assets/erpnext/js/stock_grid_report.js");
erpnext.StockAgeing = erpnext.StockGridReport.extend({
init: function(wrapper) {
this._super({
title: __("Stock Ageing"),
page: wrapper,
parent: $(wrapper).find('.layout-main'),
appframe: wrapper.appframe,
doctypes: ["Item", "Warehouse", "Stock Ledger Entry", "Item Group", "Brand", "Serial No"],
})
},
setup_columns: function() {
this.columns = [
{id: "name", name: __("Item"), field: "name", width: 300,
link_formatter: {
open_btn: true,
doctype: '"Item"'
}},
{id: "item_name", name: __("Item Name"), field: "item_name",
width: 100, formatter: this.text_formatter},
{id: "description", name: __("Description"), field: "description",
width: 200, formatter: this.text_formatter},
{id: "brand", name: __("Brand"), field: "brand", width: 100},
{id: "average_age", name: __("Average Age"), field: "average_age",
formatter: this.currency_formatter},
{id: "earliest", name: __("Earliest"), field: "earliest",
formatter: this.currency_formatter},
{id: "latest", name: __("Latest"), field: "latest",
formatter: this.currency_formatter},
{id: "stock_uom", name: "UOM", field: "stock_uom", width: 100},
];
},
filters: [
{fieldtype:"Select", label: __("Warehouse"), link:"Warehouse",
default_value: "Select Warehouse..."},
{fieldtype:"Select", label: __("Brand"), link:"Brand",
default_value: "Select Brand...", filter: function(val, item, opts) {
return val == opts.default_value || item.brand == val;
}, link_formatter: {filter_input: "brand"}},
{fieldtype:"Select", label: __("Plot By"),
options: ["Average Age", "Earliest", "Latest"]},
{fieldtype:"Date", label: __("To Date")},
{fieldtype:"Button", label: __("Refresh"), icon:"icon-refresh icon-white"},
{fieldtype:"Button", label: __("Reset Filters"), icon: "icon-filter"}
],
setup_filters: function() {
var me = this;
this._super();
this.trigger_refresh_on_change(["warehouse", "plot_by", "brand"]);
this.show_zero_check();
},
init_filter_values: function() {
this._super();
this.filter_inputs.to_date.val(dateutil.obj_to_user(new Date()));
},
prepare_data: function() {
var me = this;
if(!this.data) {
me._data = frappe.report_dump.data["Item"];
me.item_by_name = me.make_name_map(me._data);
}
this.data = [].concat(this._data);
this.serialized_buying_rates = this.get_serialized_buying_rates();
$.each(this.data, function(i, d) {
me.reset_item_values(d);
});
this.prepare_balances();
// filter out brand
this.data = $.map(this.data, function(d) {
return me.apply_filter(d, "brand") ? d : null;
});
// filter out rows with zero values
this.data = $.map(this.data, function(d) {
return me.apply_zero_filter(null, d, null, me) ? d : null;
});
},
prepare_balances: function() {
var me = this;
var to_date = dateutil.str_to_obj(this.to_date);
var data = frappe.report_dump.data["Stock Ledger Entry"];
this.item_warehouse = {};
for(var i=0, j=data.length; i<j; i++) {
var sl = data[i];
var posting_date = dateutil.str_to_obj(sl.posting_date);
if(me.is_default("warehouse") ? true : me.warehouse == sl.warehouse) {
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
// call diff to build fifo stack in item_warehouse
var diff = me.get_value_diff(wh, sl, true);
if(posting_date > to_date)
break;
}
}
$.each(me.data, function(i, item) {
var full_fifo_stack = [];
if(me.is_default("warehouse")) {
$.each(me.item_warehouse[item.name] || {}, function(i, wh) {
full_fifo_stack = full_fifo_stack.concat(wh.fifo_stack || [])
});
} else {
full_fifo_stack = me.get_item_warehouse(me.warehouse, item.name).fifo_stack || [];
}
var age_qty = total_qty = 0.0;
var min_age = max_age = null;
$.each(full_fifo_stack, function(i, batch) {
var batch_age = dateutil.get_diff(me.to_date, batch[2]);
age_qty += batch_age * batch[0];
total_qty += batch[0];
max_age = Math.max(max_age, batch_age);
if(min_age===null) min_age=batch_age;
else min_age = Math.min(min_age, batch_age);
});
item.average_age = total_qty.toFixed(2)==0.0 ? 0
: (age_qty / total_qty).toFixed(2);
item.earliest = max_age || 0.0;
item.latest = min_age || 0.0;
});
this.data = this.data.sort(function(a, b) {
var sort_by = me.plot_by.replace(" ", "_").toLowerCase();
return b[sort_by] - a[sort_by];
});
},
get_plot_data: function() {
var data = [];
var me = this;
data.push({
label: me.plot_by,
data: $.map(me.data, function(item, idx) {
return [[idx+1, item[me.plot_by.replace(" ", "_").toLowerCase() ]]]
}),
bars: {show: true},
});
return data.length ? data : false;
},
get_plot_options: function() {
var me = this;
return {
grid: { hoverable: true, clickable: true },
xaxis: {
ticks: $.map(me.data, function(item, idx) { return [[idx+1, item.name]] }),
max: 15
},
series: { downsample: { threshold: 1000 } }
}
}
});

View File

@@ -1,23 +0,0 @@
{
"creation": "2012-09-21 20:15:14.000000",
"docstatus": 0,
"doctype": "Page",
"icon": "icon-table",
"idx": 1,
"modified": "2013-07-11 14:44:08.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "stock-ageing",
"owner": "Administrator",
"page_name": "stock-ageing",
"roles": [
{
"role": "Analytics"
},
{
"role": "Material Manager"
}
],
"standard": "Yes",
"title": "Stock Ageing"
}

View File

@@ -1 +0,0 @@
Stock balances on a particular day, per warehouse.

View File

@@ -1,181 +0,0 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/stock_analytics.js");
frappe.pages['stock-balance'].onload = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Stock Balance'),
single_column: true
});
new erpnext.StockBalance(wrapper);
wrapper.appframe.add_module_icon("Stock");
}
erpnext.StockBalance = erpnext.StockAnalytics.extend({
init: function(wrapper) {
this._super(wrapper, {
title: __("Stock Balance"),
doctypes: ["Item", "Item Group", "Warehouse", "Stock Ledger Entry", "Brand",
"Stock Entry", "Project", "Serial No"],
});
},
setup_columns: function() {
this.columns = [
{id: "name", name: __("Item"), field: "name", width: 300,
formatter: this.tree_formatter},
{id: "item_name", name: __("Item Name"), field: "item_name", width: 100},
{id: "description", name: __("Description"), field: "description", width: 200,
formatter: this.text_formatter},
{id: "brand", name: __("Brand"), field: "brand", width: 100},
{id: "stock_uom", name: __("UOM"), field: "stock_uom", width: 100},
{id: "opening_qty", name: __("Opening Qty"), field: "opening_qty", width: 100,
formatter: this.currency_formatter},
{id: "inflow_qty", name: __("In Qty"), field: "inflow_qty", width: 100,
formatter: this.currency_formatter},
{id: "outflow_qty", name: __("Out Qty"), field: "outflow_qty", width: 100,
formatter: this.currency_formatter},
{id: "closing_qty", name: __("Closing Qty"), field: "closing_qty", width: 100,
formatter: this.currency_formatter},
{id: "opening_value", name: __("Opening Value"), field: "opening_value", width: 100,
formatter: this.currency_formatter},
{id: "inflow_value", name: __("In Value"), field: "inflow_value", width: 100,
formatter: this.currency_formatter},
{id: "outflow_value", name: __("Out Value"), field: "outflow_value", width: 100,
formatter: this.currency_formatter},
{id: "closing_value", name: __("Closing Value"), field: "closing_value", width: 100,
formatter: this.currency_formatter},
{id: "valuation_rate", name: __("Valuation Rate"), field: "valuation_rate", width: 100,
formatter: this.currency_formatter},
];
},
filters: [
{fieldtype:"Select", label: __("Brand"), link:"Brand", fieldname: "brand",
default_value: __("Select Brand..."), filter: function(val, item, opts) {
return val == opts.default_value || item.brand == val || item._show;
}, link_formatter: {filter_input: "brand"}},
{fieldtype:"Select", label: __("Warehouse"), link:"Warehouse", fieldname: "warehouse",
default_value: __("Select Warehouse..."), filter: function(val, item, opts, me) {
return me.apply_zero_filter(val, item, opts, me);
}},
{fieldtype:"Select", label: __("Project"), link:"Project", fieldname: "project",
default_value: __("Select Project..."), filter: function(val, item, opts, me) {
return me.apply_zero_filter(val, item, opts, me);
}, link_formatter: {filter_input: "project"}},
{fieldtype:"Date", label: __("From Date"), fieldname: "from_date"},
{fieldtype:"Label", label: __("To")},
{fieldtype:"Date", label: __("To Date"), fieldname: "to_date"},
{fieldtype:"Button", label: __("Refresh"), icon:"icon-refresh icon-white"},
{fieldtype:"Button", label: __("Reset Filters"), icon: "icon-filter"}
],
setup_plot_check: function() {
return;
},
prepare_data: function() {
this.stock_entry_map = this.make_name_map(frappe.report_dump.data["Stock Entry"], "name");
this._super();
},
prepare_balances: function() {
var me = this;
var from_date = dateutil.str_to_obj(this.from_date);
var to_date = dateutil.str_to_obj(this.to_date);
var data = frappe.report_dump.data["Stock Ledger Entry"];
this.item_warehouse = {};
this.serialized_buying_rates = this.get_serialized_buying_rates();
for(var i=0, j=data.length; i<j; i++) {
var sl = data[i];
var sl_posting_date = dateutil.str_to_obj(sl.posting_date);
if((me.is_default("warehouse") ? true : me.warehouse == sl.warehouse) &&
(me.is_default("project") ? true : me.project == sl.project)) {
var item = me.item_by_name[sl.item_code];
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
var valuation_method = item.valuation_method ?
item.valuation_method : sys_defaults.valuation_method;
var is_fifo = valuation_method == "FIFO";
var qty_diff = sl.qty;
var value_diff = me.get_value_diff(wh, sl, is_fifo);
if(sl_posting_date < from_date) {
item.opening_qty += qty_diff;
item.opening_value += value_diff;
} else if(sl_posting_date <= to_date) {
var ignore_inflow_outflow = this.is_default("warehouse")
&& sl.voucher_type=="Stock Entry"
&& this.stock_entry_map[sl.voucher_no].purpose=="Material Transfer";
if(!ignore_inflow_outflow) {
if(qty_diff < 0) {
item.outflow_qty += Math.abs(qty_diff);
} else {
item.inflow_qty += qty_diff;
}
if(value_diff < 0) {
item.outflow_value += Math.abs(value_diff);
} else {
item.inflow_value += value_diff;
}
item.closing_qty += qty_diff;
item.closing_value += value_diff;
}
} else {
break;
}
}
}
// opening + diff = closing
// adding opening, since diff already added to closing
$.each(me.item_by_name, function(key, item) {
item.closing_qty += item.opening_qty;
item.closing_value += item.opening_value;
// valuation rate
if(!item.is_group && flt(item.closing_qty) > 0)
item.valuation_rate = flt(item.closing_value) / flt(item.closing_qty);
else item.valuation_rate = 0.0
});
},
update_groups: function() {
var me = this;
$.each(this.data, function(i, item) {
// update groups
if(!item.is_group && me.apply_filter(item, "brand")) {
var parent = me.parent_map[item.name];
while(parent) {
parent_group = me.item_by_name[parent];
$.each(me.columns, function(c, col) {
if (col.formatter == me.currency_formatter && col.field != "valuation_rate") {
parent_group[col.field] = flt(parent_group[col.field]) + flt(item[col.field]);
}
});
// show parent if filtered by brand
if(item.brand == me.brand)
parent_group._show = true;
parent = me.parent_map[parent];
}
}
});
},
get_plot_data: function() {
return;
}
});

View File

@@ -1,23 +0,0 @@
{
"creation": "2012-12-27 18:57:47.000000",
"docstatus": 0,
"doctype": "Page",
"icon": "icon-table",
"idx": 1,
"modified": "2013-07-11 14:44:15.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "stock-balance",
"owner": "Administrator",
"page_name": "stock-balance",
"roles": [
{
"role": "Material Manager"
},
{
"role": "Analytics"
}
],
"standard": "Yes",
"title": "Stock Balance"
}

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils import flt, cint
def execute(filters=None):
if not filters: filters = {}
@@ -57,6 +57,7 @@ def get_stock_ledger_entries(filters):
conditions, as_dict=1)
def get_item_warehouse_batch_map(filters):
float_precision = cint(frappe.db.get_default("float_precision")) or 3
sle = get_stock_ledger_entries(filters)
iwb_map = {}
@@ -67,14 +68,14 @@ def get_item_warehouse_batch_map(filters):
}))
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no]
if d.posting_date < filters["from_date"]:
qty_dict.opening_qty += flt(d.actual_qty)
qty_dict.opening_qty += flt(d.actual_qty, float_precision)
elif d.posting_date >= filters["from_date"] and d.posting_date <= filters["to_date"]:
if flt(d.actual_qty) > 0:
qty_dict.in_qty += flt(d.actual_qty)
qty_dict.in_qty += flt(d.actual_qty, float_precision)
else:
qty_dict.out_qty += abs(flt(d.actual_qty))
qty_dict.out_qty += abs(flt(d.actual_qty, float_precision))
qty_dict.bal_qty += flt(d.actual_qty)
qty_dict.bal_qty += flt(d.actual_qty, float_precision)
return iwb_map

View File

@@ -4,10 +4,10 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import date_diff
from frappe.utils import date_diff, flt
def execute(filters=None):
columns = get_columns()
item_details = get_fifo_queue(filters)
to_date = filters["to_date"]
@@ -16,35 +16,39 @@ def execute(filters=None):
fifo_queue = item_dict["fifo_queue"]
details = item_dict["details"]
if not fifo_queue: continue
average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1])
data.append([item, details.item_name, details.description, details.item_group,
data.append([item, details.item_name, details.description, details.item_group,
details.brand, average_age, earliest_age, latest_age, details.stock_uom])
return columns, data
def get_average_age(fifo_queue, to_date):
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
batch_age = date_diff(to_date, batch[1])
age_qty += batch_age * batch[0]
total_qty += batch[0]
return (age_qty / total_qty) if total_qty else 0.0
def get_columns():
return [_("Item Code") + ":Link/Item:100", _("Item Name") + "::100", _("Description") + "::200",
_("Item Group") + ":Link/Item Group:100", _("Brand") + ":Link/Brand:100", _("Average Age") + ":Float:100",
return [_("Item Code") + ":Link/Item:100", _("Item Name") + "::100", _("Description") + "::200",
_("Item Group") + ":Link/Item Group:100", _("Brand") + ":Link/Brand:100", _("Average Age") + ":Float:100",
_("Earliest") + ":Int:80", _("Latest") + ":Int:80", _("UOM") + ":Link/UOM:100"]
def get_fifo_queue(filters):
item_details = {}
for d in get_stock_ledger_entries(filters):
item_details.setdefault(d.name, {"details": d, "fifo_queue": []})
fifo_queue = item_details[d.name]["fifo_queue"]
if d.voucher_type == "Stock Reconciliation":
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[d.name].get("qty_after_transaction", 0))
if d.actual_qty > 0:
fifo_queue.append([d.actual_qty, d.posting_date])
else:
@@ -52,7 +56,7 @@ def get_fifo_queue(filters):
while qty_to_pop:
batch = fifo_queue[0] if fifo_queue else [0, None]
if 0 < batch[0] <= qty_to_pop:
# if batch qty > 0
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
qty_to_pop -= batch[0]
fifo_queue.pop(0)
@@ -61,12 +65,14 @@ def get_fifo_queue(filters):
batch[0] -= qty_to_pop
qty_to_pop = 0
item_details[d.name]["qty_after_transaction"] = d.qty_after_transaction
return item_details
def get_stock_ledger_entries(filters):
return frappe.db.sql("""select
item.name, item.item_name, item_group, brand, description, item.stock_uom,
actual_qty, posting_date
return frappe.db.sql("""select
item.name, item.item_name, item_group, brand, description, item.stock_uom,
actual_qty, posting_date, voucher_type, qty_after_transaction
from `tabStock Ledger Entry` sle,
(select name, item_name, description, stock_uom, brand, item_group
from `tabItem` {item_conditions}) item
@@ -77,19 +83,19 @@ def get_stock_ledger_entries(filters):
order by posting_date, posting_time, sle.name"""\
.format(item_conditions=get_item_conditions(filters),
sle_conditions=get_sle_conditions(filters)), filters, as_dict=True)
def get_item_conditions(filters):
conditions = []
if filters.get("item_code"):
conditions.append("item_code=%(item_code)s")
if filters.get("brand"):
conditions.append("brand=%(brand)s")
return "where {}".format(" and ".join(conditions)) if conditions else ""
def get_sle_conditions(filters):
conditions = []
if filters.get("warehouse"):
conditions.append("warehouse=%(warehouse)s")
return "and {}".format(" and ".join(conditions)) if conditions else ""
return "and {}".format(" and ".join(conditions)) if conditions else ""

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