Compare commits

...

189 Commits

Author SHA1 Message Date
coderabbitai[bot]
741e6a7e52 📝 Add docstrings to wo-flt-issue
Docstrings generation was requested by @rtdany10.

* https://github.com/frappe/erpnext/pull/50952#issuecomment-3619564968

The following files were modified:

* `erpnext/manufacturing/doctype/work_order/work_order.py`
2025-12-06 05:39:35 +00:00
Diptanil Saha
31d55248e4 Merge pull request #50931 from diptanilsaha/gh-49357 2025-12-05 16:31:54 +05:30
Diptanil Saha
994dd425d4 Merge pull request #50943 from diptanilsaha/gh-49427 2025-12-05 16:31:08 +05:30
Khushi Rawat
97711b7e83 Merge pull request #50941 from khushi8112/asset-over-creation-from-pr-pi
fix(asset): prevent creating assets beyond purchased quantity
2025-12-05 15:11:24 +05:30
khushi8112
7012345968 fix: filter out cancelled asset in the query 2025-12-05 13:08:29 +05:30
khushi8112
7bfcdb13b1 fix: better validation message 2025-12-05 12:55:41 +05:30
diptanilsaha
d6bdbfe266 fix(sales invoice): 100% additional discount gl issue with discount accounting 2025-12-05 12:50:28 +05:30
khushi8112
2db09b3840 fix(asset): prevent creating assets beyond purchased quantity 2025-12-05 09:59:05 +05:30
rohitwaghchaure
17ff48ab92 Merge pull request #50929 from rohitwaghchaure/fixed-backflushed-basedd-on-for-job-card
fix: backflush based on for job card
2025-12-04 23:11:35 +05:30
Rohit Waghchaure
c807a7be7b fix: backflush based on for job card 2025-12-04 22:53:30 +05:30
rohitwaghchaure
44919be5a6 Merge pull request #50936 from rohitwaghchaure/fixed-stock-resevation-job-card-manufacture
fix: SRE for manufacture entry for job card
2025-12-04 22:49:51 +05:30
Rohit Waghchaure
4ad8e55d06 fix: SRE for manufacture entry for job card 2025-12-04 22:03:54 +05:30
Khushi Rawat
931f0663b1 Merge pull request #50927 from khushi8112/income-as-root-type-for-round-off-account
feat: allow income as root type for round off account
2025-12-04 13:14:12 +05:30
khushi8112
2bdcec0a7e feat: allow income as root type for round off account 2025-12-04 13:03:57 +05:30
Khushi Rawat
eb38accb33 Merge pull request #50824 from khushi8112/move-accounts-freezing-setting-to-company
refactor: Move accounts freezing setting to company
2025-12-04 12:16:46 +05:30
Khushi Rawat
c5b1af84eb Merge pull request #50879 from aerele/is-fixed-asset-set-only-once
fix: remove set_only_once from is_fixed_asset field
2025-12-04 12:14:19 +05:30
Khushi Rawat
01c14b5ce4 Merge pull request #50923 from khushi8112/fix-manual-depreciation-reset-issue
fix: do not recalculate depreciation if already exist
2025-12-04 11:56:45 +05:30
Khushi Rawat
a7155c1fdb Merge pull request #50746 from aerele/asset-depreciation-role
feat(asset): make asset depreciation failure notification role configurable
2025-12-04 11:41:06 +05:30
khushi8112
b75e7a1188 fix: do not recalculate depreciation if already exist 2025-12-04 11:37:05 +05:30
rohitwaghchaure
d6087e5d92 Merge pull request #50913 from rohitwaghchaure/fixed-github-46923
fix: variant items not fetched while making BOM for Variant Item
2025-12-04 10:07:22 +05:30
ravibharathi656
70521fb9bf fix: remove set_only_once from is_fixed_asset 2025-12-04 09:17:32 +05:30
SowmyaArunachalam
b5ee193566 chore(asset): change field name 2025-12-03 20:53:38 +05:30
Rohit Waghchaure
a0256bd798 fix: variant items not fetched while making BOM for Variant Item 2025-12-03 19:24:43 +05:30
rohitwaghchaure
6515bb04bf Merge pull request #50905 from rohitwaghchaure/fixed-github-46855
fix: LCV is not changing the valuation of the repacked item
2025-12-03 18:27:18 +05:30
rohitwaghchaure
b96b7bd046 Merge pull request #50853 from mihir-kandoi/gh33087
fix: incorrect putaway rule validation on stock reco
2025-12-03 18:26:46 +05:30
Mihir Kandoi
7c1a947cd8 Merge pull request #50906 from mihir-kandoi/gh39459 2025-12-03 18:09:37 +05:30
Rohit Waghchaure
ccbbc60585 fix: LCV is not changing the valuation of the repacked item 2025-12-03 18:07:45 +05:30
rohitwaghchaure
47af1cec1b Merge pull request #50902 from mihir-kandoi/gh39358
fix: fg qty uom in manufacture entry
2025-12-03 18:05:31 +05:30
Mihir Kandoi
ec06f4a71b fix: untranslated string in job card 2025-12-03 17:51:06 +05:30
Mihir Kandoi
d9a377108c fix: fg qty uom in manufacture entry 2025-12-03 15:45:56 +05:30
rohitwaghchaure
ed73bd6626 Merge pull request #50896 from rohitwaghchaure/fixed-github-50892
fix: quality inspection showing Not Saved
2025-12-03 13:38:15 +05:30
Rohit Waghchaure
3f78d6afed fix: quality inspection showing Not Saved 2025-12-03 13:00:06 +05:30
Khushi Rawat
4e578c4f83 fix: conflicts 2025-12-03 11:47:04 +05:30
khushi8112
404e68bdc2 fix: more patch related changes 2025-12-03 11:45:01 +05:30
khushi8112
4f33ee01cf fix: undo incorrect patch modification 2025-12-03 11:45:01 +05:30
khushi8112
c7e7e02b5b refactor: use Singles table to get acc_frozen_upto and modifier during migration 2025-12-03 11:45:01 +05:30
khushi8112
4df20a3122 fix: patch to migrate setting 2025-12-03 11:45:01 +05:30
khushi8112
29048c3364 refactor: fix incorrect conditon 2025-12-03 11:45:01 +05:30
khushi8112
09cdb943ec fix: remove duplicate method 2025-12-03 11:45:01 +05:30
khushi8112
0373f7f33f chore: validation for none type object 2025-12-03 11:45:01 +05:30
khushi8112
95877e73f0 fix: use correct date value 2025-12-03 11:45:01 +05:30
khushi8112
16f4e12854 chore: fix typo 2025-12-03 11:45:01 +05:30
khushi8112
9cc8a42074 fix: use correct field name 2025-12-03 11:45:01 +05:30
khushi8112
28febc69e8 fix: validate pending reposting till acc frozen date 2025-12-03 11:45:01 +05:30
Khushi Rawat
eee78766cd chore: resolved conflicts 2025-12-03 11:45:01 +05:30
Khushi Rawat
826c74eb71 chore: remove debug flag accidentally left in code 2025-12-03 11:45:01 +05:30
Khushi Rawat
6da10b9f97 fix: update validation and test cases 2025-12-03 11:45:01 +05:30
Khushi Rawat
cd540ab4cc chore: migration patch for account freezing fields 2025-12-03 11:45:01 +05:30
Khushi Rawat
b2e4e76b97 refactor: remove accounts freezing settings from accounts settings 2025-12-03 11:45:01 +05:30
Khushi Rawat
479e412a44 refactor: get frozen accounts settings from Company in tests 2025-12-03 11:45:01 +05:30
Khushi Rawat
d330700f39 refactor: get frozen accounts settings from Company in patches 2025-12-03 11:44:59 +05:30
Khushi Rawat
dc85babb4d refactor: get frozen accounts settings from Company in Deferred Revenue 2025-12-03 11:42:48 +05:30
Khushi Rawat
17a6392407 refactor: updated logic in depreciation and gl to validate acc frozen date company wise 2025-12-03 11:42:48 +05:30
Khushi Rawat
58db596027 feat: move frozen account settings to Company for company-specific configuration 2025-12-03 11:42:48 +05:30
ruthra kumar
7bca3bbcd8 Merge pull request #50875 from ruthra-kumar/ci_fix_broken_coverage
ci: fix coverage
2025-12-03 11:09:22 +05:30
Mihir Kandoi
b12b40f373 Merge pull request #50869 from mihir-kandoi/gh31117 2025-12-02 22:00:17 +05:30
rohitwaghchaure
72edd86561 Merge pull request #50888 from rohitwaghchaure/fixed-github-48282
fix: cost center not reset
2025-12-02 21:59:58 +05:30
Rohit Waghchaure
29f2ecbd6f fix: cost center not reset 2025-12-02 21:49:03 +05:30
rohitwaghchaure
8053303378 Merge pull request #50882 from rohitwaghchaure/fixed-mandatory-depends-on-for-rejected-inventory-dimension
fix: mandatory depends on for the rejected inventory dimension field
2025-12-02 20:39:59 +05:30
rohitwaghchaure
1a920c035b Merge pull request #50880 from rohitwaghchaure/fixed-sre-status-condition
fix: SRE validation
2025-12-02 20:36:33 +05:30
Diptanil Saha
f106d0e762 Merge pull request #50864 from aerele/allow-leaf-nodes-only 2025-12-02 20:36:04 +05:30
Rohit Waghchaure
5daa625fe8 fix: mandatory depends on for the rejected inventory dimension field 2025-12-02 20:21:07 +05:30
Diptanil Saha
9f599ee52d Merge pull request #50372 from aerele/validate-company-linked-address-field 2025-12-02 20:14:06 +05:30
Rohit Waghchaure
d82464b2f9 fix: SRE validation 2025-12-02 20:04:42 +05:30
ruthra kumar
88e94aa53a ci: code coverage action 2025-12-02 19:34:46 +05:30
rohitwaghchaure
95ff1d48ab Merge pull request #50874 from rohitwaghchaure/fixed-ux-material-request
fix: UX for auto created material request via reorder
2025-12-02 17:55:03 +05:30
rohitwaghchaure
3c6369d396 Merge pull request #50808 from aerele/support-51284
fix(stock entry): use fg item expense account for direct manufacturing entry
2025-12-02 17:31:54 +05:30
rohitwaghchaure
bee0e5d8d4 Merge pull request #50850 from aerele/support-53932
fix(barcode_scanner): set serial and batch before item to prevent FIFO override
2025-12-02 17:31:25 +05:30
Rohit Waghchaure
1e60076ade fix: UX for auto created material request via reorder 2025-12-02 17:21:50 +05:30
Diptanil Saha
4ad624be9c Merge pull request #50773 from aerele/delet-invoice-with-cancelled-repost 2025-12-02 17:12:19 +05:30
Diptanil Saha
026487dce7 Merge pull request #50846 from aerele/validate-product-bundle-stock-in-pos 2025-12-02 17:07:00 +05:30
Mihir Kandoi
9b5d215a7a fix: do cancellation procedures on WO close 2025-12-02 15:36:17 +05:30
Sudharsanan11
2612152456 test(pos): add test for product bundle negative stock validation 2025-12-02 15:19:08 +05:30
Smit Vora
75839f36ba Merge pull request #50733 from vorasmit/qb-changes 2025-12-02 14:41:58 +05:30
ravibharathi656
e08805128b fix: exclude is_group records 2025-12-02 14:09:12 +05:30
Mihir Kandoi
ba94d02cb4 Merge pull request #50834 from mihir-kandoi/pp-flaky-test 2025-12-02 13:37:11 +05:30
Mihir Kandoi
c404e3b093 fix: incorrect query/function logic 2025-12-02 13:19:26 +05:30
rohitwaghchaure
cd5b913750 Merge pull request #50856 from rohitwaghchaure/fixed-operation-read-only
fix: not able to set operation in work order
2025-12-02 12:22:46 +05:30
Khushi Rawat
dcdafc79ee Merge pull request #50794 from aerele/fix-capitalized-asset-repair-gl
fix: use asset in against_voucher while posting gl entries for capitalised asset repairs
2025-12-02 12:07:04 +05:30
Diptanil Saha
91e285efd7 Merge pull request #50149 from Dharanidharan2813/fix/payment-terms-template-fetching 2025-12-02 12:05:12 +05:30
Khushi Rawat
e9e498cbd0 Merge pull request #50793 from ljain112/fix-asset-repair-accounting-dimensions
fix: include accounting dimensions in stock entries created during asset repair.
2025-12-02 11:56:35 +05:30
Rohit Waghchaure
b24c38f332 fix: not able to set operation in work order 2025-12-02 11:18:07 +05:30
Mihir Kandoi
b4fbda4da3 Merge pull request #50854 from mihir-kandoi/employee-filter 2025-12-02 11:17:26 +05:30
Mihir Kandoi
21ec4ed911 fix: show only active employees when starting job card 2025-12-02 11:16:01 +05:30
Mihir Kandoi
7deb407206 fix: incorrect putaway rule validation on stock reco 2025-12-02 10:42:28 +05:30
Navin-S-R
8c35a6ecdd chore: reload asset doc before assertEqual 2025-12-02 00:43:15 +05:30
Pugazhendhi Velu
92ec633a5c fix(barcode_scanner): set serial and batch before item to prevent FIFO override 2025-12-01 19:11:57 +00:00
Navin-S-R
bcf6deec9a test: add unit test to validate capitalized asset repair gl entries being booked against the asset 2025-12-01 23:48:14 +05:30
MochaMind
35379294c3 fix: sync translations from crowdin (#50723) 2025-12-01 16:58:29 +01:00
rohitwaghchaure
1252fed642 Merge pull request #50844 from rohitwaghchaure/fixed-label-for-warehouse
fix: label for warehouse based on material request type
2025-12-01 21:28:21 +05:30
Rohit Waghchaure
699e9b4452 fix: label for warehouse based on material request type 2025-12-01 20:31:14 +05:30
Sudharsanan11
38b4536300 fix(pos): add negative stock validation for product bundle 2025-12-01 19:23:35 +05:30
Mihir Kandoi
25458d6ba6 fix: flaky production plan test 2025-12-01 16:04:05 +05:30
diptanilsaha
1966584804 chore: trigger GitHub actions 2025-12-01 15:27:30 +05:30
Diptanil Saha
b2ddef8340 Merge pull request #50797 from aerele/dr-cr-outstanding 2025-12-01 13:11:02 +05:30
Diptanil Saha
150c764205 Merge pull request #50814 from diptanilsaha/st54148 2025-12-01 11:45:50 +05:30
rohitwaghchaure
b855eb54b3 Merge pull request #50799 from rohitwaghchaure/fixed-negative-batch-qty-in-SCR
fix: negative batch in subcontracting receipt
2025-12-01 11:44:41 +05:30
diptanilsaha
7e8d19b0c8 fix(email campaign): send emails using bcc 2025-11-30 23:30:15 +05:30
Khushi Rawat
48783d136f Merge pull request #50749 from khushi8112/replace-use-of-publish-realtime-before-print
refactor: company details popup
2025-11-30 22:12:07 +05:30
Khushi Rawat
8b8f569da2 Merge pull request #50811 from khushi8112/extend-print-format-to-other-docs
feat: Standard Print Format for Purchase Order, Delivery Note and POS Invoice
2025-11-30 21:51:52 +05:30
khushi8112
d050cd221d fix: minor change 2025-11-30 21:29:26 +05:30
khushi8112
5cfd7ec32a refactor: generalize popup for multiple doctypes 2025-11-30 18:30:47 +05:30
khushi8112
b808a51d8f refactor: Make labels translatable 2025-11-30 17:38:50 +05:30
khushi8112
1125f96316 feat: print format with images for PO, Delivery Note and POS Invoice 2025-11-30 17:25:02 +05:30
khushi8112
725a4fcf2d feat: standard print format for POS Invoice 2025-11-30 17:00:55 +05:30
MochaMind
32cf6148aa chore: update POT file (#50810) 2025-11-30 11:24:24 +01:00
Pugazhendhi Velu
ba2411b4ee test: add test for fg item expense account in direct manufacturing 2025-11-30 06:32:35 +00:00
Pugazhendhi Velu
ce1312764f fix(stock entry): use fg item expense account for direct manufacturing entry 2025-11-30 06:32:27 +00:00
khushi8112
3a4c1a9f9a refactor: Replace use publish_realtime with msgprint for cleaner flow 2025-11-30 01:26:02 +05:30
khushi8112
945390502e feat: standard print format for Purchase Order and Delivery Note 2025-11-30 01:25:11 +05:30
ruthra kumar
ae7db7ea5a Merge pull request #50802 from Jatin3128/gh_48028
fix(accounts-payable-summary): add Show GL Balance check similar to A…
2025-11-29 11:34:05 +05:30
Jatin3128
8a7e5d0626 fix(accounts-payable-summary): add Show GL Balance check similar to Accounts Receivable Summary 2025-11-28 21:41:32 +05:30
Khushi Rawat
82b6326e0b Merge pull request #50792 from ljain112/fix-asset-repair-connection
fix: add Stock Entry link to Asset Repair doctype.
2025-11-28 17:34:38 +05:30
Khushi Rawat
e42b751dce Merge pull request #50772 from aerele/fix-asset-purchase-date
fix: use posting_date instead of bill_date from purchase invoice
2025-11-28 17:18:13 +05:30
Rohit Waghchaure
71e46b3ef5 fix: negative batch in subcontracting receipt 2025-11-28 16:26:55 +05:30
l0gesh29
765f9a9bbf fix(payment-recon): add validation for outstanding of dr_cr 2025-11-28 15:01:50 +05:30
ljain112
cdbe8b909b refactor: show_general ledger for consistency with other doctyoes 2025-11-28 14:13:24 +05:30
ruthra kumar
956e5b1b68 fix: incorrect positional param for get_field_precision util (#50764)
fix: incorrect positional param for get_field_precision util
2025-11-28 14:01:36 +05:30
ljain112
147a5ee953 fix: include accounting dimensions in stock entries created during asset repair. 2025-11-28 13:35:47 +05:30
Smit Vora
a2fadd9347 fix: use ValueWrapper consistently 2025-11-28 13:33:24 +05:30
ljain112
da7f28a3c3 fix: add Stock Entry link to Asset Repair doctype. 2025-11-28 13:05:24 +05:30
Navin S R
a7e43eddad fix: use asset in against_voucher while posting gl entries for capitalized asset repairs 2025-11-28 12:54:27 +05:30
Diptanil Saha
0520ab3c66 Merge pull request #50642 from Jatin3128/gh_48202 2025-11-28 11:50:34 +05:30
Aadhil
9145bf5563 fix: restore missing account number for Indirect Expenses in standard COA with Numbers (#50767) 2025-11-27 19:23:02 +05:30
Aadhil
355aa52cb8 feat: add Account Category field to Account (Chart of Accounts) (#50766) 2025-11-27 18:40:20 +05:30
Mihir Kandoi
cf449d8dcb Merge pull request #50777 from mihir-kandoi/sr-better-uiux 2025-11-27 17:10:45 +05:30
Diptanil Saha
98e864bea0 Merge pull request #50775 from frappe/mergify/bp/develop/pr-50558 2025-11-27 16:58:42 +05:30
Mihir Kandoi
aab7cd1ae6 chore: make unnecessary field read only and show only when required 2025-11-27 16:51:35 +05:30
Diptanil Saha
31142b2f47 chore: resolve conflict 2025-11-27 16:40:55 +05:30
Sherin KR
c5d92d7999 fix: item price not considering based on valid_upto
(cherry picked from commit dfda8e6241)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py
2025-11-27 11:04:59 +00:00
l0gesh29
d8fc369e38 fix: add validation for cancelled reposting entries 2025-11-27 14:19:32 +05:30
Navin S R
145d40dec8 fix: use posting_date instead of bill_date from purchase invoice 2025-11-27 14:06:43 +05:30
rohitwaghchaure
8a5fd5fe89 Merge pull request #50769 from rohitwaghchaure/fixed-two-primary-buttons
fix: two primary buttons
2025-11-27 11:56:00 +05:30
Rohit Waghchaure
f68515210b fix: two primary buttons 2025-11-27 11:43:21 +05:30
Hussain Nagaria
c2358c6b3f fix: incorrect positional param for get_field_precision util 2025-11-26 22:19:50 +05:30
rohitwaghchaure
8ed9ee9213 Merge pull request #50742 from rohitwaghchaure/fix-serial-batch-disassembly
fix: inward same serial / batches in disassembly which were used
2025-11-26 20:57:13 +05:30
Rohit Waghchaure
95e6c72539 fix: inward same serial / batches in disassembly which were used 2025-11-26 18:01:55 +05:30
Mihir Kandoi
2b3bdfe387 Merge pull request #50759 from mihir-kandoi/fix-bad-test 2025-11-26 17:59:55 +05:30
Mihir Kandoi
5391ca2a55 test: fix flaky test case 2025-11-26 17:40:05 +05:30
Jatin3128
0e7f75f5c0 fix(journal-entry): auto-populate bank account when user selects account (#50744)
* fix(journal-entry): auto-populate bank account when user selects account

* refactor(journal-entry): simplify get_value call and return None by default

---------

Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
2025-11-26 17:26:33 +05:30
Mihir Kandoi
d17120909c Merge pull request #50736 from mihir-kandoi/refactor-so-to-po 2025-11-26 16:55:12 +05:30
Soham Kulkarni
74a5325a95 Merge pull request #50732 from sokumon/export-all-sidebars 2025-11-26 16:22:55 +05:30
Diptanil Saha
0ca6e19a85 Merge pull request #50752 from ljain112/fix-round-off-so 2025-11-26 14:31:53 +05:30
Diptanil Saha
383c33b02a Merge pull request #50753 from diptanilsaha/gh-50083 2025-11-26 14:27:30 +05:30
diptanilsaha
079218ffbf fix: exchange_rate field visibility on invoice currency change 2025-11-26 14:20:05 +05:30
ljain112
563c2998ca fix: enhance SalesOrderController setup method to call super.setup 2025-11-26 12:51:14 +05:30
SowmyaArunachalam
d52d98666f chore: update description 2025-11-25 23:05:59 +05:30
SowmyaArunachalam
e830cca886 feat(asset): make asset depreciation failure notification role configurable 2025-11-25 22:28:26 +05:30
Mihir Kandoi
7b592d8737 feat: add provision to mass select supplier 2025-11-25 20:54:24 +05:30
Mihir Kandoi
88b262abc7 fix: more coderabbit issues 2025-11-25 16:07:35 +05:30
rohitwaghchaure
a8d3e9bacc Merge pull request #50735 from rohitwaghchaure/fixed-validation-for-batch
fix: stock reservation validation
2025-11-25 15:14:55 +05:30
ruthra kumar
264dcf8539 Merge pull request #50734 from diptanilsaha/frankfurter-api
chore: switched frankfurter domain from frankfurter.app to frankfurter.dev
2025-11-25 14:58:12 +05:30
diptanilsaha
f1f68ead7d chore: switched frankfurter api domain from api.frankfurter.app to api.frankfurter.dev 2025-11-25 13:10:48 +05:30
Mihir Kandoi
5a17dd8d6d fix: addresses not being carried forward 2025-11-25 12:58:52 +05:30
Mihir Kandoi
c93dba2895 fix: coderabbit suggestions 2025-11-25 12:33:51 +05:30
Mihir Kandoi
a436c6a503 refactor: creation of purchase order from sales order 2025-11-25 12:16:48 +05:30
Smit Vora
8235a551f0 refactor: further changes to adapt to query builder changes 2025-11-25 12:08:31 +05:30
Rohit Waghchaure
ca47ae6fd8 fix: stock reservation validation 2025-11-25 12:01:51 +05:30
sokumon
3d0b28a198 fix: re-export all sidebars 2025-11-25 11:17:29 +05:30
ruthra kumar
58e217be6a Merge pull request #50561 from aerele/hidden-fields-ledger-summary
fix(ledger-summary-report): show party group and territory
2025-11-25 11:07:23 +05:30
Logesh Periyasamy
5e58e344b2 feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (#50621)
* feat: add dynamic triggers for custom accounting dimensions

* feat: add accounting dimension trigger call in setup event

* chore: ignore cur_frm semgrep rules

* chore: move function to transaction.js
2025-11-25 10:21:17 +05:30
Mihir Kandoi
c2b8b97d7d fix: incorrect query filter when selecting primary customer adr (#50727) 2025-11-25 04:48:04 +00:00
MochaMind
fa8007f949 fix: sync translations from crowdin (#50699) 2025-11-24 19:27:59 +01:00
ruthra kumar
33135899ab Merge pull request #50706 from frappe/pot_develop_2025-11-23
chore: update POT file
2025-11-24 17:20:57 +05:30
Khushi Rawat
5806bcbb17 Merge pull request #50716 from elshafei-developer/add-missing-translate-function
fix: add missing translate function
2025-11-24 13:07:40 +05:30
El-Shafei H.
56def01240 fix: add missing translate function 2025-11-24 09:51:42 +03:00
Kavin
d01c4b68fe fix: add validation for FG Items as per BOM qty (#50579)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-11-24 06:17:14 +00:00
Mihir Kandoi
aeece36d93 Merge pull request #50712 from mihir-kandoi/gh50703 2025-11-24 11:30:50 +05:30
Mihir Kandoi
49c866db74 chore: change manufacturing course link 2025-11-24 11:12:06 +05:30
Mihir Kandoi
e2a01773a5 Merge pull request #50661 from aerele/support-53364 2025-11-24 10:38:17 +05:30
Mihir Kandoi
903e97af5f Merge pull request #50710 from mihir-kandoi/fix-phantom-subassembly 2025-11-23 21:52:09 +05:30
Mihir Kandoi
7b1d860c33 fix: subassembly inside phantom item not being fetched in production plan 2025-11-23 21:33:38 +05:30
Mihir Kandoi
5659538e67 Merge pull request #50707 from mihir-kandoi/pricing-rule-error 2025-11-23 19:37:44 +05:30
Mihir Kandoi
3b7d7aed4c fix: unknown column error 2025-11-23 19:16:39 +05:30
frappe-pr-bot
3404419a1f chore: update POT file 2025-11-23 09:36:01 +00:00
Diptanil Saha
47a6d34224 Merge pull request #50476 from aerele/support-53067 2025-11-21 22:10:37 +05:30
Pugazhendhi Velu
9194e6350a fix: apply precision for scrap items amount 2025-11-21 06:43:20 +00:00
Pugazhendhi Velu
5fd7d46986 Merge branch 'develop' of https://github.com/aerele/erpnext into support-53364 2025-11-21 05:44:52 +00:00
Pugazhendhi Velu
02941afd6a Merge branch 'develop' of https://github.com/frappe/erpnext into support-53364 2025-11-20 13:31:25 +00:00
Jatin3128
4b612c64a8 fix(payment reconciliation): added a hint that posting date can be changed on exchange gain/loss reconcile dialog 2025-11-20 12:53:29 +05:30
Pugazhendhi Velu
57f9353d90 fix(manufacturing): apply precision for bom amount and rm_cost_per_qty 2025-11-19 14:19:26 +00:00
l0gesh29
8f91919933 test: add party_group, territory in json 2025-11-18 13:42:45 +05:30
l0gesh29
231479a6e2 fix(ledger-summary-report): show party group and territory 2025-11-17 13:42:43 +05:30
Pugazhendhi Velu
e64b6db2eb test: add minimal test case 2025-11-11 13:18:05 +00:00
Pugazhendhi Velu
7fddbb6dc4 Merge branch 'develop' of https://github.com/frappe/erpnext into validate-company-linked-address-field 2025-11-11 11:44:41 +00:00
Pugazhendhi Velu
5a3fcbedb5 fix: use current_tax_amount value for base_total_taxes_and_charges 2025-11-11 10:26:31 +00:00
Pugazhendhi Velu
e10007c646 test: add test for company linked address fields 2025-11-05 15:02:57 +00:00
Pugazhendhi Velu
800a44a65f fix: add validation for company linked address fields 2025-11-05 15:02:02 +00:00
dharanidharan2813
cf1d892d60 fix: Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled 2025-10-28 17:57:09 +05:30
187 changed files with 61577 additions and 59335 deletions

View File

@@ -128,10 +128,9 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
- name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
env:
TYPE: server
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
- name: Show bench output
@@ -140,7 +139,6 @@ jobs:
- name: Upload coverage data
uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
@@ -149,7 +147,6 @@ jobs:
name: Coverage Wrap Up
needs: test
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Clone
uses: actions/checkout@v4

View File

@@ -7,6 +7,7 @@ from frappe.utils import (
cint,
date_diff,
flt,
formatdate,
get_first_day,
get_last_day,
get_link_to_form,
@@ -318,7 +319,7 @@ def get_already_booked_amount(doc, item):
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
def _book_deferred_revenue_or_expense(
item,

View File

@@ -93,8 +93,10 @@ class Account(NestedSet):
super().on_update()
def onload(self):
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
role_allowed_for_frozen_entries = frappe.db.get_value(
"Company", self.company, "role_allowed_for_frozen_entries"
)
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
self.set_onload("can_freeze_account", True)
def autoname(self):
@@ -303,10 +305,10 @@ class Account(NestedSet):
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
return
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", self.company, "role_allowed_for_frozen_entries"
)
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self):

View File

@@ -160,6 +160,14 @@ frappe.treeview_settings["Account"] = {
.options,
description: __("Optional. This setting will be used to filter in various transactions."),
},
{
fieldtype: "Link",
fieldname: "account_category",
label: __("Account Category"),
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
.options,
description: __("Optional. Used with Financial Report Template"),
},
{
fieldtype: "Float",
fieldname: "tax_rate",

View File

@@ -228,6 +228,7 @@ def get():
},
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
"account_number": "5200",
},
"root_type": "Expense",
"account_number": "5000",

View File

@@ -73,13 +73,12 @@
"calculate_depr_using_total_days",
"column_break_gjcc",
"book_asset_depreciation_entry_automatically",
"role_to_notify_on_depreciation_failure",
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
"show_balance_in_coa",
"banking_tab",
@@ -102,21 +101,6 @@
"use_legacy_budget_controller"
],
"fields": [
{
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
"fieldname": "acc_frozen_upto",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Accounts Frozen Till Date"
},
{
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
"fieldname": "frozen_accounts_modifier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
"options": "Role"
},
{
"default": "Billing Address",
"description": "Address used to determine Tax Category in transactions",
@@ -658,6 +642,13 @@
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
},
{
"description": "Users with this role will be notified if the asset depreciation gets failed",
"fieldname": "role_to_notify_on_depreciation_failure",
"fieldtype": "Link",
"label": "Role to Notify on Depreciation Failure",
"options": "Role"
}
],
"grid_page_length": 50,
@@ -666,7 +657,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-10-20 14:06:08.870427",
"modified": "2025-12-03 20:42:13.238050",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -11,7 +11,6 @@ from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
@@ -23,7 +22,6 @@ class AccountsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -50,7 +48,6 @@ class AccountsSettings(Document):
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
@@ -64,6 +61,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check
@@ -100,9 +98,6 @@ class AccountsSettings(Document):
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print()
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts()
if clear_cache:
frappe.clear_cache()
@@ -129,10 +124,6 @@ class AccountsSettings(Document):
validate_fields_for_doctype=False,
)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
def validate_and_sync_auto_reconcile_config(self):
if self.has_value_changed("auto_reconciliation_job_trigger"):
if (

View File

@@ -1,11 +1,16 @@
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
frm.set_df_property(
"frozen_accounts_modifier",
"label",
"Role Allowed to Close Books & Make Changes to Closed Periods"
);
frm.set_df_property("credit_controller", "label", "Credit Manager");
},
});
frappe.ui.form.on("Company", {
refresh: function (frm) {
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
frm.set_df_property(
"role_allowed_for_frozen_entries",
"label",
"Role Allowed to Close Books & Make Changes to Closed Periods"
);
},
});

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
} else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:06:47.653110",
"modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,8 +141,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.app":
elif self.service_provider == "frankfurter.dev":
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:

View File

@@ -100,7 +100,7 @@ class GLEntry(Document):
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.company, self.account, adv_adj)
if (
self.voucher_type == "Journal Entry"
@@ -276,7 +276,7 @@ class GLEntry(Document):
)
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
validate_party_frozen_disabled(self.company, self.party_type, self.party)
validate_account_party_type(self)
def validate_currency(self):
@@ -419,16 +419,16 @@ def update_outstanding_amt(
ref_doc.set_status(update=True)
def validate_frozen_account(account, adv_adj=None):
def validate_frozen_account(company, account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", None, "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if not frozen_accounts_modifier:
if not role_allowed_for_frozen_entries:
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
elif role_allowed_for_frozen_entries not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
@@ -442,7 +442,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

View File

@@ -720,6 +720,8 @@ $.extend(erpnext.journal_entry, {
}
},
});
} else {
erpnext.journal_entry.clear_fields(frm, dt, dn);
}
},
set_amount_on_last_row: function (frm, dt, dn) {
@@ -744,4 +746,13 @@ $.extend(erpnext.journal_entry, {
}
refresh_field("accounts");
},
clear_fields: function (frm, dt, dn) {
let row = locals[dt][dn];
row.party_type = null;
row.party = null;
row.bank_account = null;
frm.refresh_field("accounts");
},
});

View File

@@ -1719,6 +1719,9 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
"party_type": party_type,
"account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency,
"bank_account": (
frappe.db.get_value("Bank Account", {"account": account, "company": company}) or None
),
# The date used to retreive the exchange rate here is the date passed in
# as an argument to this function. It is assumed to be the date on which the balance is sought
"exchange_rate": get_exchange_rate(

View File

@@ -1874,7 +1874,7 @@ class PaymentEntry(AccountsController):
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
self.base_total_taxes_and_charges += current_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@@ -159,7 +159,7 @@ class PaymentLedgerEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.company, self.account, adv_adj)
if not self.delinked:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()

View File

@@ -334,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
{
fieldtype: "HTML",
options: "<b> New Journal Entry will be posted for the difference amount </b>",
options: __(
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
).bold(),
},
],
primary_action: () => {

View File

@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
)
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
reconcile_dr_or_cr = (

View File

@@ -20,6 +20,11 @@ from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.stock_ledger import is_negative_stock_allowed
class ProductBundleStockValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
@@ -395,32 +400,67 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
if not d.serial_and_batch_bundle:
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
if frappe.db.exists("Product Bundle", d.item_code):
(
availability,
is_stock_item,
is_negative_stock_allowed,
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
else:
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
if is_negative_stock_allowed:
continue
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
if isinstance(availability, list):
error_msgs = []
for item in availability:
if flt(item["available"]) < flt(item["required"]):
error_msgs.append(
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
frappe.bold(item["item_code"]),
frappe.bold(flt(item["required"], 2)),
frappe.bold(flt(item["available"], 2)),
)
)
if error_msgs:
frappe.throw(
_(
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
).format(
d.idx,
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
"<br>".join(error_msgs),
),
title=_("Insufficient Stock for Product Bundle Items"),
exc=ProductBundleStockValidationError,
)
else:
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
if is_stock_item and flt(availability) <= 0:
frappe.throw(
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
d.idx, item_code, warehouse
),
title=_("Item Out of Stock"),
)
elif is_stock_item and flt(availability) < flt(d.stock_qty):
frappe.throw(
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
d.idx,
item_code,
warehouse,
frappe.bold(flt(availability, 2)),
frappe.bold(flt(d.stock_qty, 2)),
),
title=_("Insufficient Stock"),
)
def validate_is_pos_using_sales_invoice(self):
self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
@@ -858,8 +898,6 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
@@ -876,6 +914,26 @@ def get_stock_availability(item_code, warehouse):
return 0, is_stock_item, False
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
is_stock_item = True
bundle = frappe.get_doc("Product Bundle", item_code)
availabilities = []
for bundle_item in bundle.items:
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
available = bin_qty - reserved_qty
availabilities.append(
{
"item_code": bundle_item.item_code,
"required": bundle_item.qty * item_qty,
"available": available,
}
)
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)

View File

@@ -1024,6 +1024,84 @@ class TestPOSInvoice(IntegrationTestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
frappe.set_user("Administrator")
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
init_user_and_profile()
frappe.set_user("Administrator")
warehouse = "_Test Warehouse - _TC"
company = "_Test Company"
# Create stock sub-items
sub_item_a = "_Test Bundle SubA"
if not frappe.db.exists("Item", sub_item_a):
create_item(
item_code=sub_item_a,
is_stock_item=1,
)
sub_item_b = "_Test Bundle SubB"
if not frappe.db.exists("Item", sub_item_b):
create_item(
item_code=sub_item_b,
is_stock_item=1,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
if not frappe.db.exists("Item", bundle_item):
create_item(
item_code=bundle_item,
is_stock_item=0,
)
if not frappe.db.exists("Product Bundle", bundle_item):
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
pos_inv_sufficient = create_pos_invoice(
item=bundle_item,
qty=1,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
pos_inv_sufficient.insert()
pos_inv_sufficient.submit()
pos_inv_sufficient.cancel()
pos_inv_sufficient.delete()
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
pos_inv_insufficient = create_pos_invoice(
item=bundle_item,
qty=2,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
pos_inv_insufficient.save()
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
frappe.set_user("test@example.com")
def create_pos_invoice(**args):
args = frappe._dict(args)

View File

@@ -244,7 +244,7 @@ def get_other_conditions(conditions, values, args):
conditions += " and " + group_condition
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date"
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')

View File

@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import create_item
class TestProcessDeferredAccounting(IntegrationTestCase):
def test_creation_of_ledger_entry_on_submit(self):
"""test creation of gl entries on submission of document"""
change_acc_settings(acc_frozen_upto="2023-05-31", book_deferred_entries_based_on="Months")
change_acc_settings(acc_frozen_till_date="2023-05-31", book_deferred_entries_based_on="Months")
deferred_account = create_account(
account_name="Deferred Revenue for Accounts Frozen",
@@ -92,8 +92,10 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
pda.cancel()
def change_acc_settings(acc_frozen_upto="", book_deferred_entries_based_on="Days"):
def change_acc_settings(
company="_Test Company", acc_frozen_till_date=None, book_deferred_entries_based_on="Days"
):
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.acc_frozen_upto = acc_frozen_upto
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
frappe.db.set_value("Company", company, "accounts_frozen_till_date", acc_frozen_till_date)
acc_settings.save()

View File

@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);

View File

@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {

View File

@@ -282,59 +282,6 @@ class SalesInvoice(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
def before_print(self, settings=None):
from frappe.contacts.doctype.address.address import get_address_display_list
super().before_print(settings)
company_details = frappe.get_value(
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
)
required_fields = [
company_details.get("company_logo"),
company_details.get("phone_no"),
company_details.get("email"),
]
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
frappe.msgprint(
_(
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
)
)
return
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
frappe.msgprint(
_(
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
)
)
return
address_display_list = get_address_display_list("Company", self.company)
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
required_fields.append(self.company_address)
required_fields.append(address_line)
if not all(required_fields):
frappe.publish_realtime(
"sales_invoice_before_print",
{
"company_logo": company_details.get("company_logo"),
"website": company_details.get("website"),
"phone_no": company_details.get("phone_no"),
"email": company_details.get("email"),
"address_line": address_line,
"company": self.company,
"company_address": self.company_address,
"name": self.name,
},
user=frappe.session.user,
)
def validate(self):
self.validate_auto_set_posting_time()
super().validate()
@@ -1678,7 +1625,11 @@ class SalesInvoice(SellingController):
)
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
if (
flt(item.base_net_amount, item.precision("base_net_amount"))
or item.is_fixed_asset
or enable_discount_accounting
):
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue
@@ -2948,59 +2899,6 @@ def get_loyalty_programs(customer):
return lp_details
@frappe.whitelist()
def save_company_master_details(name, company, details):
from frappe.utils import validate_email_address
if isinstance(details, str):
details = frappe.parse_json(details)
if details.get("email"):
validate_email_address(details.get("email"), throw=True)
company_fields = ["company_logo", "website", "phone_no", "email"]
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
if company_fields_to_update:
frappe.db.set_value("Company", company, company_fields_to_update)
company_address = details.get("company_address")
if details.get("address_line1"):
address_doc = frappe.get_doc(
{
"doctype": "Address",
"address_title": details.get("address_title"),
"address_type": details.get("address_type"),
"address_line1": details.get("address_line1"),
"address_line2": details.get("address_line2"),
"city": details.get("city"),
"state": details.get("state"),
"pincode": details.get("pincode"),
"country": details.get("country"),
"is_your_company_address": 1,
"links": [{"link_doctype": "Company", "link_name": company}],
}
)
address_doc.insert()
company_address = address_doc.name
if company_address:
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
if not company_address_display or details.get("address_line1"):
from frappe.query_builder import DocType
SalesInvoice = DocType("Sales Invoice")
(
frappe.qb.update(SalesInvoice)
.set(SalesInvoice.company_address, company_address)
.set(SalesInvoice.company_address_display, get_address_display(company_address))
.where(SalesInvoice.name == name)
).run()
return True
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@@ -63,7 +63,8 @@ class TestSalesInvoice(ERPNextTestSuite):
set_default_account_for_mode_of_payment(
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
for company in frappe.get_all("Company", pluck="name"):
frappe.db.set_value("Company", company, "accounts_frozen_till_date", None)
@change_settings(
"Accounts Settings",
@@ -3398,8 +3399,8 @@ class TestSalesInvoice(ERPNextTestSuite):
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
@IntegrationTestCase.change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
@@ -3407,6 +3408,7 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, si.submit)
si.posting_date = getdate()
si.submit()
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_over_billing_case_against_delivery_note(self):
@@ -3473,7 +3475,7 @@ class TestSalesInvoice(ERPNextTestSuite):
si.save()
si.submit()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", getdate("2019-01-31"))
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31"))
pda1 = frappe.get_doc(
dict(

View File

@@ -27,7 +27,7 @@ class TestSubscription(IntegrationTestCase):
make_plans()
create_parties()
reset_settings()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
def tearDown(self):
frappe.db.rollback()

View File

@@ -301,7 +301,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(
@@ -404,7 +406,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
check_freezing_date(gl_map[0]["posting_date"], gl_map[0]["company"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
@@ -765,7 +767,7 @@ def make_reverse_gl_entries(
make_entry(new_gle, adv_adj, "Yes")
def check_freezing_date(posting_date, adv_adj=False):
def check_freezing_date(posting_date, company, adv_adj=False):
"""
Nobody can do GL Entries where posting date is before freezing date
except authorized person
@@ -774,17 +776,17 @@ def check_freezing_date(posting_date, adv_adj=False):
Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto:
frozen_accounts_modifier = frappe.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
if acc_frozen_till_date:
frozen_accounts_modifier = frappe.db.get_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
):
frappe.throw(
_("You are not authorized to add or update entries before {0}").format(
formatdate(acc_frozen_upto)
formatdate(acc_frozen_till_date)
)
)

View File

@@ -1,108 +1,108 @@
<style>
.letter-head {
border-radius: 18px;
padding-right: 12px;
margin-left: 12px;
margin-right: 12px;
}
.letter-head {
border-radius: 18px;
padding-right: 12px;
margin-left: 12px;
margin-right: 12px;
}
.letter-head td{
padding: 0px !important;
}
.invoice-header {
width: 100%;
}
.logo-cell {
width: 100px;
text-align: center;
position: relative;
}
.logo-container {
width: 90px;
display: block;
}
.logo-container img {
max-width: 90px;
max-height: 90px;
display: inline-block;
border-radius: 15px;
}
.company-details {
width: 40%;
align-content: center;
}
.company-name {
font-size: 14px;
font-weight: bold;
color: #171717;
margin-bottom: 4px;
}
.invoice-info-cell {
float: right;
vertical-align: top;
}
.invoice-info {
margin-bottom: 2px;
}
.invoice-label {
color: #7C7C7C;
display: inline-block;
width: 60px;
margin-right: 5px;
}
.letter-head td {
padding: 0px !important;
}
.invoice-header {
width: 100%;
}
.logo-cell {
width: 100px;
text-align: center;
position: relative;
}
.logo-container {
width: 90px;
display: block;
}
.logo-container img {
max-width: 90px;
max-height: 90px;
display: inline-block;
border-radius: 15px;
}
.company-details {
width: 40%;
align-content: center;
}
.company-name {
font-size: 14px;
font-weight: bold;
color: #171717;
margin-bottom: 4px;
}
.invoice-info-cell {
float: right;
vertical-align: top;
}
.invoice-info {
margin-bottom: 2px;
}
.invoice-label {
color: #7c7c7c;
display: inline-block;
margin-right: 5px;
}
</style>
<table class="invoice-header">
<tbody>
<tr>
<td class="logo-cell" style="vertical-align: middle !important;">
<div class="logo-container">
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
{% if company_logo %}
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
{% endif %}
</div>
</td>
<tbody>
<tr>
<td class="logo-cell" style="vertical-align: middle !important">
<div class="logo-container">
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
company_logo %}
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo" />
{% endif %}
</div>
</td>
<td class="company-details">
<div class="company-name">
{{ doc.company }}
</div>
{% if doc.company_address %}
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
<td class="company-details">
<div class="company-name">{{ doc.company }}</div>
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2", "city",
"state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address %} {{
company_address.address_line1 or "" }}<br />
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
{% endif %}
</td>
{{ company_address.get("address_line1") or "" }}<br>
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
{% endif %}
</td>
<td class="invoice-info-cell">
{% set website = frappe.db.get_value("Company", doc.company, "website") %} {% set email =
frappe.db.get_value("Company", doc.company, "email") %} {% set phone_no =
frappe.db.get_value("Company", doc.company, "phone_no") %}
<td class="invoice-info-cell">
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Invoice:") }}</span>
<span>{{ doc.name }}</span>
</div>
{% if company_details.website %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Website:") }}</span>
<span>{{ company_details.website }}</span>
</div>
{% endif %}
{% if company_details.email %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Email:") }}</span>
<span>{{ company_details.email }}</span>
</div>
{% endif %}
{% if company_details.phone_no %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Contact:") }}</span>
<span>{{ company_details.phone_no }}</span>
</div>
{% endif %}
</td>
</tr>
</tbody>
<div class="invoice-info">
<span class="invoice-label">{{ doc.doctype }}</span>
<span>{{ doc.name }}</span>
</div>
{% if website %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Website:") }}</span>
<span>{{ website }}</span>
</div>
{% endif %} {% if email %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Email:") }}</span>
<span>{{ email }}</span>
</div>
{% endif %} {% if phone_no %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Contact:") }}</span>
<span>{{ phone_no }}</span>
</div>
{% endif %}
</td>
</tr>
</tbody>
</table>

View File

@@ -1,7 +1,7 @@
<style>
.print-format-preview {
margin-top: 12px;
}
margin-top: 12px;
}
.letter-head {
border-radius: 18px;
background: #f8f8f8;
@@ -82,41 +82,43 @@
{% endif %}
<div class="company-name">{{ doc.company }}</div>
<div class="company-address">
{% if doc.company_address %}
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
{{ company_address.address_line1 or "" }}<br />
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
{{ company_address.city or "" }}, {{ company_address.state or "" }}
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2",
"city", "state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address
%} {{ company_address.address_line1 or "" }}<br />
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
{% endif %}
</div>
</td>
<td style="vertical-align: top">
<div style="height: 90px; margin-bottom: 10px; text-align: right">
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
<div class="invoice-title">{{ doc.doctype }}</div>
<div class="invoice-number">{{ doc.name }}</div>
<br />
</div>
<div style="text-align: left; float: right" class="other-details">
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
{% if company_details.website %}
<div>
<span class="contact-title">{{ _("Website:") }}</span
><span class="contact-value">{{ company_details.website }}</span>
</div>
{% endif %}
{% if company_details.email %}
<div>
<span class="contact-title">{{ _("Email:") }}</span
><span class="contact-value">{{ company_details.email }}</span>
</div>
{% endif %}
{% if company_details.phone_no %}
<div>
<span class="contact-title">{{ _("Contact:") }}</span
><span class="contact-value">{{ company_details.phone_no }}</span>
</div>
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email",
"phone_no"], as_dict=True) %} {% set website = company_details.website %} {% set email =
company_details.email %} {% set phone_no = company_details.phone_no %} {% if website %}
<div>
<span class="contact-title">{{ _("Website:") }}</span
><span class="contact-value">{{ website }}</span>
</div>
{% endif %} {% if email %}
<div>
<span class="contact-title">{{ _("Email:") }}</span
><span class="contact-value">{{ email }}</span>
</div>
{% endif %} {% if phone_no %}
<div>
<span class="contact-title">{{ _("Contact:") }}</span
><span class="contact-value">{{ phone_no }}</span>
</div>
{% endif %}
</div>
</td>

View File

@@ -795,7 +795,7 @@ def get_payment_terms_template(party_name, party_type, company=None):
return template
def validate_party_frozen_disabled(party_type, party_name):
def validate_party_frozen_disabled(company, party_type, party_name):
if frappe.flags.ignore_party_validation:
return
@@ -805,10 +805,10 @@ def validate_party_frozen_disabled(party_type, party_name):
if party.disabled:
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
elif party.get("is_frozen"):
frozen_accounts_modifier = frappe.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if frozen_accounts_modifier not in frappe.get_roles():
if role_allowed_for_frozen_entries not in frappe.get_roles():
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
elif party_type == "Employee":

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{
fieldname: "show_gl_balance",
label: __("Show GL Balance"),
fieldtype: "Check",
},
],
onload: function (report) {

View File

@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
for party, party_dict in self.party_total.items():
if flt(party_dict.outstanding, self.currency_precision) == 0:
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
def get_gl_balance(report_date, company):
def get_gl_balance(report_date, company, account_type):
if account_type == "Payable":
balance_calc_fields = ["party", {"SUM": [{"SUB": ["credit", "debit"]}], "as": "balance"}]
else:
balance_calc_fields = ["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}]
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}],
fields=balance_calc_fields,
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,

View File

@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
party_type = self.filters.party_type
doctype = qb.DocType(party_type)
party_details_fields = [
doctype.name.as_("party"),
f"{scrub(party_type)}_name",
f"{scrub(party_type)}_group",
]
if party_type == "Customer":
party_details_fields.append(doctype.territory)
conditions = self.get_party_conditions(doctype)
query = (
qb.from_(doctype)
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
.where(Criterion.all(conditions))
)
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
from frappe.desk.reportview import build_match_conditions
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
}
]
columns += [
{
"label": _("Opening Balance"),
@@ -214,35 +245,6 @@ class PartyLedgerSummaryReport:
},
]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100})
return columns

View File

@@ -186,6 +186,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
expected = {
"party": "_Test Customer",
"customer_name": "_Test Customer",
"customer_group": "_Test Customer Group",
"territory": "_Test Territory",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 100.0,
@@ -213,6 +215,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
expected = {
"party": "_Test Customer",
"customer_name": "_Test Customer",
"customer_group": "_Test Customer Group",
"territory": "_Test Territory",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 100.0,

View File

@@ -198,7 +198,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm");
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");

View File

@@ -7,6 +7,7 @@ import math
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
cint,
flt,
@@ -121,7 +122,7 @@ class Asset(AccountsController):
def validate(self):
self.validate_category()
self.validate_precision()
self.validate_linked_purchase_docs()
self.validate_linked_purchase_documents()
self.set_purchase_doc_row_item()
self.validate_asset_values()
self.validate_asset_and_reference()
@@ -151,9 +152,9 @@ class Asset(AccountsController):
if not schedule_doc:
schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
schedule_doc.asset = self.name
schedule_doc.create_depreciation_schedule(row)
schedule_doc.save()
schedules.append(schedule_doc.name)
schedule_doc.create_depreciation_schedule(row)
schedule_doc.save()
schedules.append(schedule_doc.name)
self.show_schedule_creation_message(schedules)
@@ -422,20 +423,67 @@ class Asset(AccountsController):
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
frappe.throw(_("Available-for-use Date should be after purchase date"))
def validate_linked_purchase_docs(self):
for doctype_field, doctype_name in [
def validate_linked_purchase_documents(self):
for fieldname, doctype in [
("purchase_receipt", "Purchase Receipt"),
("purchase_invoice", "Purchase Invoice"),
]:
linked_doc = getattr(self, doctype_field, None)
if linked_doc:
docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset.").format(
get_link_to_form(doctype_name, linked_doc)
)
purchase_doc = getattr(self, fieldname, None)
if not purchase_doc:
continue
if frappe.db.get_value(doctype, purchase_doc, "docstatus") == 0:
frappe.throw(
_("{0} is in Draft. Submit it before creating the Asset.").format(
get_link_to_form(doctype, purchase_doc)
)
)
self.validate_asset_qty_with_purchase_doc(doctype, purchase_doc)
def validate_asset_qty_with_purchase_doc(self, doctype, purchase_doc):
Asset = frappe.qb.DocType("Asset")
if doctype == "Purchase Invoice":
asset_filter = Asset.purchase_invoice == purchase_doc
else:
asset_filter = Asset.purchase_receipt == purchase_doc
existing_asset_qty = (
frappe.qb.from_(Asset)
.select(IfNull(Sum(Asset.asset_quantity), 0))
.where((Asset.item_code == self.item_code) & (Asset.name != self.name) & (Asset.docstatus != 2))
.where(asset_filter)
).run()[0][0]
PurchaseDoc = frappe.qb.DocType(doctype)
PurchaseDocItems = frappe.qb.DocType(f"{doctype} Item")
purchased_qty = (
frappe.qb.from_(PurchaseDoc)
.join(PurchaseDocItems)
.on(PurchaseDoc.name == PurchaseDocItems.parent)
.select(IfNull(Sum(PurchaseDocItems.qty), 0))
.where(PurchaseDoc.name == purchase_doc)
.where(PurchaseDocItems.item_code == self.item_code)
).run()[0][0]
if (existing_asset_qty + self.asset_quantity) > purchased_qty:
frappe.throw(
_(
"<b>Cannot create asset.</b><br><br>"
"You're trying to create <b>{0} asset(s)</b> from {2} {3}.<br>"
"However, only <b>{1} item(s)</b> were purchased and <b>{4} asset(s)</b> already exist against {5}."
).format(
self.asset_quantity,
purchased_qty,
doctype,
get_link_to_form(doctype, purchase_doc),
existing_asset_qty,
purchase_doc,
)
)
def validate_gross_and_purchase_amount(self):
if self.is_existing_asset:
@@ -1212,7 +1260,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
return {
"company": purchase_doc.company,
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
"purchase_date": purchase_doc.get("posting_date"),
"net_purchase_amount": flt(first_item.base_net_amount),
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),

View File

@@ -96,13 +96,28 @@ def get_depreciable_assets_data(date):
.orderby(a.creation, order=Order.desc)
)
acc_frozen_upto = get_acc_frozen_upto()
if acc_frozen_upto:
res = res.where(ds.schedule_date > acc_frozen_upto)
companies_with_frozen_limits = get_companies_with_frozen_limits()
res = res.run()
for company, frozen_upto in companies_with_frozen_limits.items():
res = res.where((a.company != company) | (ds.schedule_date > frozen_upto))
return res
return res.run()
def get_companies_with_frozen_limits():
companies_with_frozen_limits = {}
for d in frappe.get_all(
"Company", fields=["name", "accounts_frozen_till_date", "role_allowed_for_frozen_entries"]
):
if not d.accounts_frozen_till_date:
continue
if (
d.role_allowed_for_frozen_entries not in frappe.get_roles()
and frappe.session.user != "Administrator"
):
companies_with_frozen_limits[d.name] = getdate(d.accounts_frozen_till_date)
return companies_with_frozen_limits
def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
@@ -111,20 +126,6 @@ def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
make_depreciation_entry(depr_schedule_name, disposal_date)
def get_acc_frozen_upto():
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
if not acc_frozen_upto:
return
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
return getdate(acc_frozen_upto)
return
def get_credit_debit_accounts_for_asset(asset_category, company):
# Returns credit and debit accounts for the given asset category and company.
(_, accumulated_depr_account, depr_expense_account) = get_depreciation_accounts(asset_category, company)
@@ -307,7 +308,8 @@ def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
def notify_depr_entry_posting_error(failed_asset_names, error_log_names):
recipients = get_users_with_role("Accounts Manager")
user_role = frappe.db.get_single_value("Accounts Settings", "role_to_notify_on_depreciation_failure")
recipients = get_users_with_role(user_role or "Accounts Manager")
if not recipients:
recipients = get_users_with_role("System Manager")

View File

@@ -73,14 +73,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -144,6 +137,26 @@ frappe.ui.form.on("Asset Repair", {
frm.set_value("repair_cost", 0);
}
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
__("View")
);
}
},
});
frappe.ui.form.on("Asset Repair Consumed Item", {

View File

@@ -260,8 +260,13 @@
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-04 23:06:43.644846",
"links": [
{
"link_doctype": "Stock Entry",
"link_fieldname": "asset_repair"
}
],
"modified": "2025-11-28 13:04:34.921098",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -7,6 +7,9 @@ from frappe.query_builder import DocType
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -231,6 +234,12 @@ class AssetRepair(AccountsController):
}
)
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -242,8 +251,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -320,7 +328,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,

View File

@@ -3,6 +3,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
@@ -322,6 +324,31 @@ class TestAssetRepair(IntegrationTestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations + (

View File

@@ -36,6 +36,7 @@
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
@@ -270,6 +271,14 @@
"label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
}
],
"grid_page_length": 50,
@@ -278,7 +287,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-08-20 22:13:38.506889",
"modified": "2025-11-20 12:59:09.925862",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -44,6 +44,7 @@ class BuyingSettings(Document):
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None
use_transaction_date_exchange_rate: DF.Check
validate_consumed_qty: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -317,6 +317,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
erpnext.buying.BuyingController
) {
setup() {
this.setup_accounting_dimension_triggers();
this.frm.custom_make_buttons = {
"Purchase Receipt": "Purchase Receipt",
"Purchase Invoice": "Purchase Invoice",

View File

@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
frm.set_query("supplier_primary_contact", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
supplier: doc.name,
type: "Contact",
},
};
});
frm.set_query("supplier_primary_address", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
link_doctype: "Supplier",
link_name: doc.name,
supplier: doc.name,
type: "Address",
},
};
});
@@ -133,6 +135,14 @@ frappe.ui.form.on("Supplier", {
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
frm.set_query("supplier_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
get_supplier_group_details: function (frm) {
frappe.call({

View File

@@ -219,19 +219,25 @@ class Supplier(TransactionBase):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
contact = frappe.qb.DocType("Contact")
type = filters.get("type")
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link")
return (
frappe.qb.from_(contact)
query = (
frappe.qb.from_(type_doctype)
.join(dynamic_link)
.on(contact.name == dynamic_link.parent)
.select(contact.name, contact.email_id)
.on(type_doctype.name == dynamic_link.parent)
.select(type_doctype.name)
.where(
(dynamic_link.link_name == supplier)
& (dynamic_link.link_doctype == "Supplier")
& (contact.name.like(f"%{txt}%"))
& (type_doctype.name.like(f"%{txt}%"))
)
).run(as_dict=False)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
@@ -310,6 +311,31 @@ class AccountsController(TransactionBase):
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
self.validate_company_linked_addresses()
def validate_company_linked_addresses(self):
address_fields = []
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
address_fields = ["dispatch_address_name", "company_address"]
elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
address_fields = ["billing_address", "shipping_address"]
for field in address_fields:
address = self.get(field)
if address and not frappe.db.exists(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": "Company",
"link_name": self.company,
},
):
frappe.throw(
_("{0} does not belong to the {1}.").format(
_(self.meta.get_label(field)), bold(self.company)
)
)
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -364,6 +390,24 @@ class AccountsController(TransactionBase):
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
cancelled_entries = (
frappe.qb.from_(dt)
.select(dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
.run(as_dict=True)
)
if cancelled_entries:
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
frappe.throw(
_(
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
"Kindly delete these entries before continuing."
).format(self.name, entries)
)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)
@@ -2313,7 +2357,7 @@ class AccountsController(TransactionBase):
def validate_party(self):
party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party)
validate_party_frozen_disabled(self.company, party_type, party)
def get_party(self):
party_type = None
@@ -4167,3 +4211,130 @@ def update_gl_dict_with_regional_fields(doc, gl_dict):
def update_gl_dict_with_app_based_fields(doc, gl_dict):
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
frappe.get_attr(method)(doc, gl_dict)
@frappe.whitelist()
def get_missing_company_details(doctype, docname):
from frappe.contacts.doctype.address.address import get_address_display_list
company = frappe.db.get_value(doctype, docname, "company")
if doctype == "Purchase Order":
company_address = frappe.db.get_value(doctype, docname, "billing_address")
else:
company_address = frappe.db.get_value(doctype, docname, "company_address")
company_details = frappe.get_value(
"Company", company, ["company_logo", "website", "phone_no", "email"], as_dict=True
)
required_fields = [
company_details.get("company_logo"),
company_details.get("phone_no"),
company_details.get("email"),
]
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
frappe.msgprint(
_(
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
)
)
return
if not company_address and not frappe.has_permission(doctype, "write", throw=False):
frappe.msgprint(
_(
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
)
)
return
address_display_list = get_address_display_list("Company", company)
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
required_fields.append(company_address)
required_fields.append(address_line)
if all(required_fields):
return False
return {
"company_logo": company_details.get("company_logo"),
"website": company_details.get("website"),
"phone_no": company_details.get("phone_no"),
"email": company_details.get("email"),
"address_line": address_line,
"company": company,
"company_address": company_address,
"name": docname,
}
@frappe.whitelist()
def update_company_master_and_address(current_doctype, name, company, details):
from frappe.utils import validate_email_address
if isinstance(details, str):
details = frappe.parse_json(details)
if details.get("email"):
validate_email_address(details.get("email"), throw=True)
company_fields = ["company_logo", "website", "phone_no", "email"]
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
if company_fields_to_update:
frappe.db.set_value("Company", company, company_fields_to_update)
company_address = details.get("company_address")
if details.get("address_line1"):
address_doc = frappe.get_doc(
{
"doctype": "Address",
"address_title": details.get("address_title"),
"address_type": details.get("address_type"),
"address_line1": details.get("address_line1"),
"address_line2": details.get("address_line2"),
"city": details.get("city"),
"state": details.get("state"),
"pincode": details.get("pincode"),
"country": details.get("country"),
"is_your_company_address": 1,
"links": [{"link_doctype": "Company", "link_name": company}],
}
)
address_doc.insert()
company_address = address_doc.name
update_doc_company_address(current_doctype, name, company_address, details)
def update_doc_company_address(current_doctype, docname, company_address, details):
if not company_address:
return
address_field_map = {
"Purchase Order": ("billing_address", "billing_address_display"),
"Sales Invoice": ("company_address", "company_address_display"),
"Delivery Note": ("company_address", "company_address_display"),
"POS Invoice": ("company_address", "company_address_display"),
}
address_field, display_field = address_field_map.get(
current_doctype, ("company_address", "company_address_display")
)
current_display = frappe.db.get_value(current_doctype, docname, display_field)
if current_display and not details.get("address_line1"):
return
from frappe.query_builder import DocType
DocType = DocType(current_doctype)
(
frappe.qb.update(DocType)
.set(getattr(DocType, address_field), company_address)
.set(getattr(DocType, display_field), get_address_display(company_address))
.where(DocType.name == docname)
).run()

View File

@@ -186,7 +186,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:

View File

@@ -25,6 +25,7 @@ from erpnext.controllers.sales_and_purchase_return import (
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
@@ -1216,6 +1217,12 @@ class StockController(AccountsController):
],
}.get(self.doctype)
qty_field = {
"Sales Invoice": "qty",
"Delivery Note": "qty",
"Stock Entry": "fg_completed_qty",
}.get(self.doctype)
reserved_batches_data = self.get_reserved_batches(batches)
items = self.items
if self.doctype == "Stock Entry":
@@ -1236,6 +1243,17 @@ class StockController(AccountsController):
if row.voucher_no == value:
continue
batch_qty = get_batch_qty(
row.batch_no,
row.warehouse,
posting_date=self.posting_date,
posting_time=self.posting_time,
consider_negative_batches=True,
)
if item.get(qty_field) < batch_qty:
continue
frappe.throw(
_(
"The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}."
@@ -1264,6 +1282,7 @@ class StockController(AccountsController):
doctype.voucher_type,
doctype.voucher_no,
doctype.item_code,
doctype.warehouse,
)
.where((doctype.docstatus == 1) & (child_doc.batch_no.isin(batches)))
).run(as_dict=True)
@@ -1626,7 +1645,7 @@ class StockController(AccountsController):
rule = frappe.db.get_value(
"Putaway Rule",
{"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)},
["name", "disable"],
["stock_capacity", "name", "disable"],
as_dict=True,
)
if rule:
@@ -1645,7 +1664,11 @@ class StockController(AccountsController):
rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
rule_map[rule_name]["item"] = item.get("item_code")
rule_map[rule_name]["qty_put"] = 0
rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
rule_map[rule_name]["capacity"] = (
rule.stock_capacity
if self.doctype == "Stock Reconciliation"
else get_available_putaway_capacity(rule_name)
)
rule_map[rule_name]["qty_put"] += flt(stock_qty)
for rule, values in rule_map.items():

View File

@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_auto_batch_nos,
@@ -549,7 +550,7 @@ class SubcontractingController(StockController):
if item.get("serial_and_batch_bundle"):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
data = []
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
@@ -590,7 +591,7 @@ class SubcontractingController(StockController):
to_remove = []
for item in data:
if item.is_phantom_item:
data += self.__get_materials_from_bom(
data += self._get_materials_from_bom(
item.rm_item_code, item.bom_no, exploded_item=exploded_item
)
to_remove.append(item)
@@ -758,7 +759,11 @@ class SubcontractingController(StockController):
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
elif (
item_details.has_batch_no
and not row.serial_and_batch_bundle
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
):
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
@@ -783,6 +788,11 @@ class SubcontractingController(StockController):
)
consumed_qty -= d.get("qty")
def batch_has_not_available(self, batch_no, qty_required):
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
return batch_qty < qty_required
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
@@ -921,7 +931,7 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype or (
self.backflush_based_on == "BOM" or self.is_return
):
for bom_item in self.__get_materials_from_bom(
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor

View File

@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_purchase_order,
prepare_data_for_internal_transfer,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -2432,3 +2435,34 @@ class TestAccountsController(IntegrationTestCase):
# Second return should only get remaining discount (100 - 60 = 40)
self.assertEqual(return_si_2.discount_amount, -40)
def test_company_linked_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
company_address = make_address(
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
)
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
company_address.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.shipping_address = company_address.name
po.save()
po.billing_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, po.save)
po.billing_address = company_address.name
po.reload()
po.save()

View File

@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
recipients=recipient_list,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -460,10 +460,12 @@ frappe.ui.form.on("BOM", {
);
has_template_rm.forEach((d) => {
let bom_qty = dialog.fields_dict.qty?.value || 1;
dialog.fields_dict.items.df.data.push({
item_code: d.item_code,
variant_item_code: "",
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
source_warehouse: d.source_warehouse,
operation: d.operation,
});

View File

@@ -916,7 +916,9 @@ class BOM(WebsiteGenerator):
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
)
d.base_amount = d.amount * flt(self.conversion_rate)
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
self.quantity, self.precision("quantity")
@@ -939,7 +941,10 @@ class BOM(WebsiteGenerator):
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
d.precision("amount"),
)
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)
@@ -1408,7 +1413,7 @@ def validate_bom_no(item, bom_no):
@frappe.whitelist()
def get_children(parent=None, return_all=True, fetch_phantom_items=False, is_root=False, **filters):
def get_children(parent=None, is_root=False, **filters):
if not parent or parent == "BOM":
frappe.msgprint(_("Please select a BOM"))
return
@@ -1420,13 +1425,10 @@ def get_children(parent=None, return_all=True, fetch_phantom_items=False, is_roo
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True)
filters = [["parent", "=", frappe.form_dict.parent]]
if not return_all:
filters.append(["is_phantom_item", "=", cint(fetch_phantom_items)])
bom_items = frappe.get_all(
"BOM Item",
fields=["item_code", "bom_no as value", "stock_qty", "qty", "is_phantom_item", "bom_no"],
filters=filters,
filters=[["parent", "=", frappe.form_dict.parent]],
order_by="idx",
)

View File

@@ -7,6 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, flt, sbool
from pypika.terms import ValueWrapper
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
@@ -373,7 +374,7 @@ def get_children(doctype=None, parent=None, **kwargs):
"parent as parent_id",
"qty",
"idx",
"'BOM Creator Item' as doctype",
ValueWrapper("BOM Creator Item").as_("doctype"),
"name",
"uom",
"rate",

View File

@@ -221,6 +221,9 @@ frappe.ui.form.on("Job Card", {
label: __("Select Employees"),
options: "Job Card Time Log",
fieldname: "employees",
filters: {
status: "Active",
},
},
(d) => {
frm.events.start_timer(frm, from_time, d.employees);

View File

@@ -220,17 +220,12 @@ class JobCard(Document):
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
frappe.throw(
_(
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
).format(bold(self.operation), form_link),
title=_("Extra Job Card Quantity"),
)
def set_sub_operations(self):
if not self.sub_operations and self.operation:
@@ -945,6 +940,36 @@ class JobCard(Document):
},
)
def set_consumed_qty_in_job_card_item(self, ste_doc):
jc_item_names = [row.job_card_item for row in ste_doc.get("items") if row.get("job_card_item")]
if not jc_item_names:
return
se = frappe.qb.DocType("Stock Entry")
sed = frappe.qb.DocType("Stock Entry Detail")
query = (
frappe.qb.from_(sed)
.join(se)
.on(sed.parent == se.name)
.select(sed.job_card_item, Sum(sed.qty))
.where(
(sed.job_card_item.isin(jc_item_names)) & (se.docstatus == 1) & (se.purpose == "Manufacture")
)
.groupby(sed.job_card_item)
)
itemwise_consumed_qty = frappe._dict(query.run(as_list=True))
for row in ste_doc.items:
if not row.get("job_card_item"):
continue
consumed_qty = flt(itemwise_consumed_qty.get(row.job_card_item, 0.0))
frappe.db.set_value("Job Card Item", row.job_card_item, "consumed_qty", consumed_qty)
def set_transferred_qty_in_job_card_item(self, ste_doc):
def _get_job_card_items_transferred_qty(ste_doc):
from frappe.query_builder.functions import Sum
@@ -1170,14 +1195,16 @@ class JobCard(Document):
)
if row.completed_qty < current_operation_qty:
msg = f"""The completed quantity {bold(current_operation_qty)}
of an operation {bold(self.operation)} cannot be greater
than the completed quantity {bold(row.completed_qty)}
of a previous operation
{bold(row.operation)}.
"""
frappe.throw(_(msg))
frappe.throw(
_(
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
).format(
bold(current_operation_qty),
bold(self.operation),
bold(row.completed_qty),
bold(row.operation),
)
)
def validate_work_order(self):
if self.is_work_order_closed():

View File

@@ -17,6 +17,7 @@
"required_qty",
"column_break_9",
"transferred_qty",
"consumed_qty",
"allow_alternative_item"
],
"fields": [
@@ -100,20 +101,28 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:56.943741",
"modified": "2025-12-04 14:30:19.472294",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -15,6 +15,7 @@ class JobCardItem(Document):
from frappe.types import DF
allow_alternative_item: DF.Check
consumed_qty: DF.Float
description: DF.Text | None
item_code: DF.Link | None
item_group: DF.Link | None

View File

@@ -1661,23 +1661,6 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
] += d.get("qty")
sub_assembly_items = {k[:2]: v for k, v in sub_assembly_items.items()}
data = []
for row in doc.get("po_items"):
get_sub_assembly_items(
[],
frappe._dict(),
row.get("bom_no"),
data,
row.get("planned_qty"),
doc.get("company"),
warehouse=doc.get("sub_assembly_warehouse"),
skip_available_sub_assembly_item=doc.get("skip_available_sub_assembly_item"),
fetch_phantom_items=True,
)
for d in data:
sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("stock_qty")
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
data["include_exploded_items"] = 1
@@ -1896,9 +1879,8 @@ def get_sub_assembly_items(
warehouse=None,
indent=0,
skip_available_sub_assembly_item=False,
fetch_phantom_items=False,
):
data = get_bom_children(parent=bom_no, return_all=False, fetch_phantom_items=fetch_phantom_items)
data = get_bom_children(parent=bom_no)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
@@ -1921,31 +1903,32 @@ def get_sub_assembly_items(
elif warehouse:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
bom_data.append(
frappe._dict(
{
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
if bin_details.get(d.item_code)
else 0,
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
"required_qty": required_qty,
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
if bin_details.get(d.item_code)
else 0,
"main_bom": bom_no,
}
if not d.is_phantom_item:
bom_data.append(
frappe._dict(
{
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
if bin_details.get(d.item_code)
else 0,
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
"required_qty": required_qty,
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
if bin_details.get(d.item_code)
else 0,
"main_bom": bom_no,
}
)
)
)
if d.value:
get_sub_assembly_items(
@@ -1958,7 +1941,6 @@ def get_sub_assembly_items(
warehouse,
indent=indent + 1,
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
fetch_phantom_items=fetch_phantom_items,
)
@@ -2016,22 +1998,14 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
return reserved_qty_for_production_plan - reserved_qty_for_production
@frappe.request_cache
def get_non_completed_production_plans():
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Item")
return (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(table.name)
.distinct()
.where(
(table.docstatus == 1)
& (table.status.notin(["Completed", "Closed"]))
& (child.planned_qty > child.ordered_qty)
)
.where((table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])))
).run(pluck="name")

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