Compare commits

..

52 Commits

Author SHA1 Message Date
Frappe PR Bot
e9a9224ec4 chore(release): Bumped to Version 16.25.0
# [16.25.0](https://github.com/frappe/erpnext/compare/v16.24.0...v16.25.0) (2026-06-24)

### Features

* **accounts:** add configurable job timeout for Process Period Closing Voucher ([e71b066](e71b066eec))
2026-06-24 10:43:40 +00:00
ruthra kumar
b08de1f1e5 Merge pull request #56427 from frappe/mergify/bp/version-16/pr-56418
refactor: configurable timeout on process pcv (backport #56417) (backport #56418)
2026-06-24 16:12:00 +05:30
ruthra kumar
e46be25e9f chore: resolve conflicts
(cherry picked from commit df3c821f98)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-06-24 15:36:07 +05:30
ruthra kumar
570c67bb34 refactor: patch, display depends on and json changes
(cherry picked from commit 3da7eefebb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
(cherry picked from commit c33d7e5d7b)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-06-24 10:00:58 +00:00
ruthra kumar
e71b066eec feat(accounts): add configurable job timeout for Process Period Closing Voucher
Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings
so admins can tune the enqueue timeout for PCV background jobs without
a code change. All three `frappe.enqueue` calls in
`process_period_closing_voucher.py` now read this value at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 13b6c4a165)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
(cherry picked from commit c97be8abe1)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2026-06-24 10:00:57 +00:00
Frappe PR Bot
e9a26b5086 chore(release): Bumped to Version 16.24.0
# [16.24.0](https://github.com/frappe/erpnext/compare/v16.23.1...v16.24.0) (2026-06-23)

### Bug Fixes

* add customer type in the list view ([c788106](c788106011))
* add partially transferred status and fix button visibility for partial material transfer on job card ([570ef45](570ef45e46))
* add validation and tests for set_status ([bcd72a7](bcd72a7fec))
* address product bundle review comments ([c066880](c066880978))
* apply docstatus filter to exclude cancelled Work Orders in Serial No ([58d5f39](58d5f39e0a))
* **budget:** ambiguous error message for budget assignment validation (backport [#56390](https://github.com/frappe/erpnext/issues/56390)) ([#56392](https://github.com/frappe/erpnext/issues/56392)) ([2b6f2c2](2b6f2c2f9c))
* clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (backport [#55903](https://github.com/frappe/erpnext/issues/55903)) ([#56171](https://github.com/frappe/erpnext/issues/56171)) ([d363186](d3631860db))
* **coa_importer:** allow importing COA through `import_coa` only for `Accounts Manager` (backport [#56132](https://github.com/frappe/erpnext/issues/56132)) ([#56140](https://github.com/frappe/erpnext/issues/56140)) ([e6e5591](e6e5591088))
* company default handling in purchase transactions made from project ([29323cb](29323cb0b1))
* customer master form cleanup ([8b56b7b](8b56b7ba0e))
* disable is_debit_note while creating credit note ([c7dbedb](c7dbedbfdc))
* disarding stock entry fix ([5372254](537225494c))
* **err:** add missing permission check on `get_account_details` ([88ce356](88ce356d62))
* escape user image url on various templates (backport [#56269](https://github.com/frappe/erpnext/issues/56269)) ([#56271](https://github.com/frappe/erpnext/issues/56271)) ([3b734f4](3b734f4d5d))
* fetch party types based on account type in journal entry and refactor SQL to query builder ([bbb3181](bbb3181c6e))
* honor account freezing date when cancelling vouchers ([f4b827c](f4b827cb3d))
* **journal entry:** validate opening entry against pcv on save ([f8aa4c7](f8aa4c730c))
* lock budget distribution table and guard against null distribution rows ([2b28b7e](2b28b7e694))
* **manufacturing:** make item_code mandatory in Job Card Item ([d40c36a](d40c36a4b1))
* party specific item doesnt work if there are 2 suppliers with same item ([3df7a28](3df7a28476))
* **payment_entry:** recompute base amount when exchange rate changes (backport [#56136](https://github.com/frappe/erpnext/issues/56136)) ([#56398](https://github.com/frappe/erpnext/issues/56398)) ([1d0edf1](1d0edf1b9a))
* placement of fields ([#56257](https://github.com/frappe/erpnext/issues/56257)) ([3f53af8](3f53af8b1f))
* **pos:** remove redundant opening balance dialog onchange handler (backport [#54591](https://github.com/frappe/erpnext/issues/54591)) ([#56403](https://github.com/frappe/erpnext/issues/56403)) ([4555c32](4555c323af))
* preserve stock ageing on non-serial reconciliation ([846e0a9](846e0a9f06))
* removing the document naming series dialog and moving to framework ([97279c7](97279c7e26))
* **report_utils:** remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin` ([bf58393](bf58393fda))
* resolve backport merge conflicts in customer.json ([77121f2](77121f2a41))
* resolve backport merge conflicts in supplier.json ([a80de9b](a80de9bd01))
* **stock:** allow partial raw material picking/transfer from work order ([8e3fbab](8e3fbab94a))
* **stock:** apply precision to the additional cost amount in stock entry ([6ac699d](6ac699d3bb))
* **stock:** define qi exception classes in exceptions file ([42c121a](42c121a750))
* **stock:** enable quality inspection for all Stock Entry purposes ([a631035](a6310351fd))
* **stock:** propagate renamed attribute values to variant items ([dbc831e](dbc831e008))
* **stock:** update transfer status for mixed transfer flows ([84a1a51](84a1a51023))
* **stock:** update variant attributes on value rename ([3110ab1](3110ab1c57))
* **stock:** update voucher valuaion rate in sle (backport [#55960](https://github.com/frappe/erpnext/issues/55960)) ([#56263](https://github.com/frappe/erpnext/issues/56263)) ([82e1221](82e1221dc9))
* submittable product bundle issues ([7a1def0](7a1def07e9))
* supplier master form cleanup ([e5c9e7a](e5c9e7abdc))
* supplier status in list view ([2035fac](2035fac494))
* tax.base_tax_amount as none when payment entry created using API ([43b355e](43b355eaf6))
* type def in get_linked_payments ([#56100](https://github.com/frappe/erpnext/issues/56100)) ([8e21af0](8e21af0a63))
* update reference doctype mapping and field visibility in bank guarantee ([dc9ae20](dc9ae20db8))
* update round off account functions to accept document context for regional overrides (backport [#55758](https://github.com/frappe/erpnext/issues/55758)) ([#55771](https://github.com/frappe/erpnext/issues/55771)) ([f5d05b9](f5d05b969b))
* update weighted average rate calculation to consider returned and consumed quantities ([35e0604](35e06045bd))

### Features

* add batch-level option to allow negative stock for batch ([1f075d4](1f075d4bbf))
* allocate full actual charge to stock items only (e.g. Freight) (backport [#56102](https://github.com/frappe/erpnext/issues/56102)) ([#56222](https://github.com/frappe/erpnext/issues/56222)) ([9469889](9469889bd5))
* **opening invoice creation tool:** add project to opening invoice child row (backport [#54662](https://github.com/frappe/erpnext/issues/54662)) ([#56401](https://github.com/frappe/erpnext/issues/56401)) ([d54938f](d54938fa64))
* party aliases ([768425e](768425ebf1))

### Performance Improvements

* composite index on (serial_no, warehouse, posting_datetime) for Serial and Batch Entry (backport [#56032](https://github.com/frappe/erpnext/issues/56032)) ([#56166](https://github.com/frappe/erpnext/issues/56166)) ([c0dab55](c0dab55fcc))
2026-06-23 21:37:59 +00:00
Diptanil Saha
872c86e223 Merge pull request #56359 from frappe/version-16-hotfix
chore: release v16
2026-06-24 03:06:16 +05:30
Frappe PR Bot
d47aa4917a chore(release): Bumped to Version 16.23.1
## [16.23.1](https://github.com/frappe/erpnext/compare/v16.23.0...v16.23.1) (2026-06-19)

### Bug Fixes

* type def in get_linked_payments (backport [#56100](https://github.com/frappe/erpnext/issues/56100)) ([#56133](https://github.com/frappe/erpnext/issues/56133)) ([6569fa2](6569fa2b9f))
2026-06-19 08:39:21 +00:00
mergify[bot]
6569fa2b9f fix: type def in get_linked_payments (backport #56100) (#56133)
fix: type def in get_linked_payments (#56100)

(cherry picked from commit 8e21af0a63)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-06-19 08:37:40 +00:00
Frappe PR Bot
b1ca423e11 chore(release): Bumped to Version 16.23.0
# [16.23.0](https://github.com/frappe/erpnext/compare/v16.22.0...v16.23.0) (2026-06-16)

### Bug Fixes

* **accounts:** removed whitelist on `get_balance_on` (backport [#55956](https://github.com/frappe/erpnext/issues/55956)) ([#55965](https://github.com/frappe/erpnext/issues/55965)) ([5cdb481](5cdb481bdf))
* adapt Product Bundle queries to v16 schema ([b1895e9](b1895e9a9a))
* add permission checks in accounts whitelisted methods ([b9eb52b](b9eb52b171))
* added doctype filter validation for sales person wise transaction summary report (backport [#55812](https://github.com/frappe/erpnext/issues/55812)) ([#55818](https://github.com/frappe/erpnext/issues/55818)) ([c06046d](c06046df8f))
* apply user permissions to receivable/payable reports ([b05abbc](b05abbc53b))
* **banking:** miscellaneous bug fixes (backport [#55492](https://github.com/frappe/erpnext/issues/55492)) ([#55979](https://github.com/frappe/erpnext/issues/55979)) ([3243e42](3243e4237a))
* **bom:** allow zero qty for secondary items (Co-Product, By-Product, Scrap, Additional Finished Good) ([7d95aca](7d95acabe7))
* **bom:** fetch routing operations when Routing is selected ([#55813](https://github.com/frappe/erpnext/issues/55813)) ([f3caed3](f3caed378b))
* **budget:** add root_type filter on account field ([#55934](https://github.com/frappe/erpnext/issues/55934)) ([0906737](0906737bd3))
* bump BOM secondary item metadata timestamp ([4f96073](4f96073d59))
* check for bank account permission when updating balance (backport [#56016](https://github.com/frappe/erpnext/issues/56016)) ([#56017](https://github.com/frappe/erpnext/issues/56017)) ([2bfb5fe](2bfb5fe8b8))
* **company:** replaced "this company" with company name on delete transactions dialog (backport [#56021](https://github.com/frappe/erpnext/issues/56021)) ([#56023](https://github.com/frappe/erpnext/issues/56023)) ([f4bf35a](f4bf35aa31))
* converted whitelist non class methods to class methods ([b616206](b616206848))
* create_raw_materials_supplied method not found ([d51ad0d](d51ad0d19f))
* exclude non-stock item's tax value from stock valuation ([3d6c7f4](3d6c7f4512))
* fiscal year check on validation (backport [#55930](https://github.com/frappe/erpnext/issues/55930)) ([#55938](https://github.com/frappe/erpnext/issues/55938)) ([f3e23d4](f3e23d4e6f))
* **get_exchange_rate:** using get_single_value to fetch `disabled` value from `currency_exchange_settings` ([2606d66](2606d660af))
* **inactive_customers:** add allowlist for doctype filter and migrate to qb ([f43af66](f43af66246))
* **Lead:** stop storing Gravatar image URLs for Leads (backport [#55880](https://github.com/frappe/erpnext/issues/55880)) ([#55882](https://github.com/frappe/erpnext/issues/55882)) ([0b03f18](0b03f18a39))
* **manufacture:** preserve user-entered rate for secondary items with zero cost allocation ([5090389](509038971c))
* multiple issues related to BOM Creator ([c10a331](c10a331a22))
* opportunity creation from contact us page (backport [#55841](https://github.com/frappe/erpnext/issues/55841)) ([#55867](https://github.com/frappe/erpnext/issues/55867)) ([85e6b8d](85e6b8d27b))
* pass source cost center to target cost center ([558415b](558415b1e7))
* pemission for whitelist functions ([dd56e80](dd56e80512))
* permission in bom compare tool ([c3b66bc](c3b66bce1e))
* permissions in workstation file ([0fea933](0fea93388d))
* prefetch batchwise valuations before streaming SLEs in stock ageing ([018f06d](018f06d8d1))
* prevent exchange rate flow from transaction to payment ([d7de07b](d7de07bfd4))
* provision to recalculate valuation rate during reposting ([5b61304](5b61304046))
* recalculate incoming rate in SLE for purchase documents during repost ([d3cc684](d3cc684899))
* regression issues related to security fixes ([be1aa0e](be1aa0e5eb))
* remove duplicate field ([#55941](https://github.com/frappe/erpnext/issues/55941)) ([545d052](545d052b4e))
* remove ignore_permissions from get_party_details signature ([#55491](https://github.com/frappe/erpnext/issues/55491)) ([b2e7fd7](b2e7fd7957))
* remove wrapper for list items in error messages (backport [#54848](https://github.com/frappe/erpnext/issues/54848)) ([#55973](https://github.com/frappe/erpnext/issues/55973)) ([4ba5557](4ba5557a3f))
* resolve pre-commit formatting and missing UOM in stock balance test ([96465a7](96465a7936))
* resolve v16 BOM backport conflicts ([c7cc0fe](c7cc0fec0f))
* restricting currency_exchange_settings write permission only to system manager ([79fd176](79fd176a8e))
* semgrep translation issue ([bcd38c1](bcd38c17d6))
* set options Email for customer_email field in appointment ([6d038c5](6d038c5e71))
* set transaction currency on payment entry gl entries ([#55989](https://github.com/frappe/erpnext/issues/55989)) ([3586df8](3586df8ed6))
* show company name in delete transactions confirmation dialog ([6a3c973](6a3c973b0f))
* show inactive product bundles in item where used ([#55769](https://github.com/frappe/erpnext/issues/55769)) ([2bea9ae](2bea9ae2a5))
* show user disable audit log ([ca61e5a](ca61e5a214))
* **stock:** make uom mandatory in item uom table ([7f8c7d2](7f8c7d2f44))
* **stock:** show only batched items in batch item selector ([11b3841](11b3841418))
* **stock:** update stock value calculation in stock balance report ([00794e0](00794e0815))
* sync employee user status after save ([a83002a](a83002aae6))
* test case ([9fcb730](9fcb730090))
* UI/UX issues in new banking module (backport [#54824](https://github.com/frappe/erpnext/issues/54824)) ([#55951](https://github.com/frappe/erpnext/issues/55951)) ([961a9ad](961a9ad321))
* update system manager permissions (backport [#55978](https://github.com/frappe/erpnext/issues/55978)) ([#56005](https://github.com/frappe/erpnext/issues/56005)) ([ab856d3](ab856d370a))
* updated role based permission for terms and conditions doctype ([#55674](https://github.com/frappe/erpnext/issues/55674)) ([254290a](254290a88e))
* use frankfurter v2 by default for new install ([471ab66](471ab662f6))

### Features

* add alternate UOM balance columns to Stock Balance report ([cd1f872](cd1f872912)), closes [#52953](https://github.com/frappe/erpnext/issues/52953)
* Allow to edit stock UOM qty for Stock Entry ([ece43bd](ece43bd79b))
* **banking:** PDF statement importer and overriding column mapping (backport [#55559](https://github.com/frappe/erpnext/issues/55559)) ([#55993](https://github.com/frappe/erpnext/issues/55993)) ([125bda9](125bda95ed))
* **currency exchange settings:** frankfurter v2 support ([e804bf3](e804bf33ba))
* **invoices:** add tooltip description to Update Stock checkbox ([#55868](https://github.com/frappe/erpnext/issues/55868)) ([12be632](12be63229a))
* new banking module (backport [#54720](https://github.com/frappe/erpnext/issues/54720)) ([#55917](https://github.com/frappe/erpnext/issues/55917)) ([67cc59c](67cc59c5ca))
* **selling:** surface and respect disabled Product Bundles (backport [#55791](https://github.com/frappe/erpnext/issues/55791)) ([0c1a508](0c1a5082bd))
* sticky columns in reports ([3317158](331715815c))
2026-06-16 21:31:15 +00:00
Diptanil Saha
c1e77c04fd Merge pull request #55981 from frappe/version-16-hotfix
chore: release v16
2026-06-17 02:59:09 +05:30
Frappe PR Bot
054b20a2ae chore(release): Bumped to Version 16.22.0
# [16.22.0](https://github.com/frappe/erpnext/compare/v16.21.1...v16.22.0) (2026-06-10)

### Bug Fixes

* **accounts:** include asset items in purchase receipt validation ([#55150](https://github.com/frappe/erpnext/issues/55150)) ([0984c86](0984c86583))
* Add authorization checks on internal functions (backport [#55709](https://github.com/frappe/erpnext/issues/55709)) ([#55726](https://github.com/frappe/erpnext/issues/55726)) ([2ae6451](2ae6451f10))
* add company filter to Budget Against dimension options ([bfc6f44](bfc6f44fb0))
* add custom dimensions filters in Gross and Net profit report (backport [#55110](https://github.com/frappe/erpnext/issues/55110)) ([#55584](https://github.com/frappe/erpnext/issues/55584)) ([5d7e69d](5d7e69d8cf))
* Add likely missing escapes (backport [#55574](https://github.com/frappe/erpnext/issues/55574)) ([#55581](https://github.com/frappe/erpnext/issues/55581)) ([6a503f8](6a503f834c))
* aggregate child cost center data in Budget Variance Report ([4f2611c](4f2611cbe8))
* allow specific methods to run ([c9a5b00](c9a5b0026e))
* bypass project permission check when updating consumed material … (backport [#55645](https://github.com/frappe/erpnext/issues/55645)) ([#55707](https://github.com/frappe/erpnext/issues/55707)) ([4471666](4471666c8c))
* **cheque_print_template:** print format creation from cheque print template requires system manager (backport [#55708](https://github.com/frappe/erpnext/issues/55708)) ([#55712](https://github.com/frappe/erpnext/issues/55712)) ([38dd298](38dd2982f3))
* disallow BOM finished good item in secondary items table (backport [#55710](https://github.com/frappe/erpnext/issues/55710)) ([#55719](https://github.com/frappe/erpnext/issues/55719)) ([ecd3a19](ecd3a19912))
* do not allow to make changes in SABB after submit ([a03e3bf](a03e3bfe9f))
* don't allow to submit job card with hold status ([b4c850d](b4c850da1c))
* drop ignore_permissions handling from add_ac ([c0cf9aa](c0cf9aa1a7))
* duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (backport [#55421](https://github.com/frappe/erpnext/issues/55421)) ([#55609](https://github.com/frappe/erpnext/issues/55609)) ([808e51d](808e51db19))
* handle multi-select stock ageing filters ([#55775](https://github.com/frappe/erpnext/issues/55775)) ([e48ffe6](e48ffe6ef0))
* handle separator rows in financial statement formatter ([d8afc00](d8afc00ab5))
* include CRM Deal in `quotation to` filters ([f7e6542](f7e6542bcd))
* item report view ([7dfae51](7dfae51044))
* **item:** format integer numeric variant attributes without decimals (backport [#55561](https://github.com/frappe/erpnext/issues/55561)) ([#55564](https://github.com/frappe/erpnext/issues/55564)) ([fffba84](fffba84868))
* linter issue ([dd83705](dd837052ef))
* minor fixes in report print formats ([#55151](https://github.com/frappe/erpnext/issues/55151)) ([80f7aff](80f7aff3f9))
* move Company filter at the start ([3fb6437](3fb6437d26))
* naming series issue ([6eecf07](6eecf0701e))
* prevent double rounding in inclusive tax calculations (backport [#52512](https://github.com/frappe/erpnext/issues/52512)) ([#55570](https://github.com/frappe/erpnext/issues/55570)) ([37b61f0](37b61f06ae))
* prevent leakage of party-derived fields in cross doctype transactions (backport [#55336](https://github.com/frappe/erpnext/issues/55336)) ([#55579](https://github.com/frappe/erpnext/issues/55579)) ([7904385](7904385b90))
* prevent negative amounts in common party JE on return invoices (backport [#55034](https://github.com/frappe/erpnext/issues/55034)) ([#55064](https://github.com/frappe/erpnext/issues/55064)) ([9eb0e3c](9eb0e3c82e))
* prevent selling items from sample retention warehouse (backport [#55613](https://github.com/frappe/erpnext/issues/55613)) ([#55634](https://github.com/frappe/erpnext/issues/55634)) ([84d205f](84d205f553))
* **process statement of accounts:** validate pdf_name and validate permission before triggering send_auto_email (backport [#55781](https://github.com/frappe/erpnext/issues/55781)) ([#55783](https://github.com/frappe/erpnext/issues/55783)) ([e15879a](e15879acd1))
* **profit-and-loss-statement-report:** margin calculation the report showing null% for empty cell ([04fe76b](04fe76bf83))
* **profit-and-loss-statement:** margin calculation the report showing null% for empty cell ([b3d3f13](b3d3f13fc5))
* **profit-loss-report:** handle zero base values and prevent null% display ([90ac065](90ac065930))
* remove item name from update items dialog item code column (backport [#55718](https://github.com/frappe/erpnext/issues/55718)) ([#55723](https://github.com/frappe/erpnext/issues/55723)) ([497c3a5](497c3a5e83))
* restrict already invoiced qty in intercompany purchase invoice ([#55768](https://github.com/frappe/erpnext/issues/55768)) ([e90a6ec](e90a6ecf1c))
* **selling:** consider delivered qty (backport [#55597](https://github.com/frappe/erpnext/issues/55597)) ([#55607](https://github.com/frappe/erpnext/issues/55607)) ([142ab3c](142ab3ce2a))
* simplify New Zealand sales accounts ([93de14c](93de14c421))
* skip empty spacer rows in compute_growth_view_data (P&L growth view) ([18afcf0](18afcf0c01))
* spelling of Payment Reconciliation in sidebar (backport [#55599](https://github.com/frappe/erpnext/issues/55599)) ([#55602](https://github.com/frappe/erpnext/issues/55602)) ([40cf77e](40cf77e7f0))
* sql injection ([02a29a8](02a29a85a7))
* Stock Reservation blocks Subcontracting operation within the same Work Order ([1c0dace](1c0dace3d6))
* **stock:** add validation for work order seial nos and batch nos (backport [#55604](https://github.com/frappe/erpnext/issues/55604)) ([#55605](https://github.com/frappe/erpnext/issues/55605)) ([7de77a8](7de77a8916))
* **stock:** change valuation rate column label in stock ledger entry/report (backport [#55323](https://github.com/frappe/erpnext/issues/55323)) ([#55394](https://github.com/frappe/erpnext/issues/55394)) ([c6560be](c6560be58d))
* **stock:** set stock received but not billed account for purchase ([#55149](https://github.com/frappe/erpnext/issues/55149)) ([90667b2](90667b2de2))
* **subscription:** bill on creation and keep status in sync with invoices (backport [#55615](https://github.com/frappe/erpnext/issues/55615)) ([#55701](https://github.com/frappe/erpnext/issues/55701)) ([0f069e1](0f069e13da))
* **subscription:** correct billing/deferred bugs and tighten guards (backport [#55554](https://github.com/frappe/erpnext/issues/55554)) ([#55610](https://github.com/frappe/erpnext/issues/55610)) ([dee7bd8](dee7bd8d64))
* **taxes:** add category and add_deduct_tax fields to tax entries (backport [#55753](https://github.com/frappe/erpnext/issues/55753)) ([#55773](https://github.com/frappe/erpnext/issues/55773)) ([9a6fae9](9a6fae9fdd))
* update add_total_row_account to control blank row addition ([e8a6933](e8a6933ff3))
* update formatter to handle blank rows in financial statements ([f657503](f657503ea3))
* update items respects workflow "Only Allow Edit For" role ([#55667](https://github.com/frappe/erpnext/issues/55667)) ([76b9b6a](76b9b6a34e))
* use new_doc with field allowlist in CRM integration endpoints ([d941ccf](d941ccfe3c))
* **UX:** Accounts settings cleanup (backport [#55470](https://github.com/frappe/erpnext/issues/55470)) ([#55603](https://github.com/frappe/erpnext/issues/55603)) ([3917415](3917415368))
* **UX:** stock settings form cleanup ([f6f542f](f6f542fadc))
* validate fg and materials qty in the disassemble entry ([4453c10](4453c1072a))
* work order status should be in process if material transfer is skipped (backport [#55641](https://github.com/frappe/erpnext/issues/55641) to version-16-hotfix) ([#55642](https://github.com/frappe/erpnext/issues/55642)) ([32011c3](32011c3364))

### Features

* add item where used report ([#55714](https://github.com/frappe/erpnext/issues/55714)) ([8f85cce](8f85cce4cf))
* add New Zealand chart of accounts ([107a446](107a446d98))
* added cost of goods sold (backport [#54974](https://github.com/frappe/erpnext/issues/54974)) ([#55552](https://github.com/frappe/erpnext/issues/55552)) ([20af709](20af7093ac))
* create sales invoice from pick list (backport [#55594](https://github.com/frappe/erpnext/issues/55594)) ([#55635](https://github.com/frappe/erpnext/issues/55635)) ([743afc9](743afc972d))
* item prices list view ([#54853](https://github.com/frappe/erpnext/issues/54853)) ([12c1940](12c1940e0b))
* show non stock items and secondary items in work order (backport [#55631](https://github.com/frappe/erpnext/issues/55631)) ([#55636](https://github.com/frappe/erpnext/issues/55636)) ([3f983c9](3f983c9e4d))

### Performance Improvements

* batch status check for on-hold/closed documents, remove N+1 queries (backport [#54798](https://github.com/frappe/erpnext/issues/54798)) ([#55573](https://github.com/frappe/erpnext/issues/55573)) ([0274afe](0274afe560))
* **transaction:** exit early before backend query (backport [#55556](https://github.com/frappe/erpnext/issues/55556)) ([#55558](https://github.com/frappe/erpnext/issues/55558)) ([6a1c384](6a1c384f9b))
2026-06-10 00:25:19 +00:00
Mihir Kandoi
03f6b7a50e Merge pull request #55764 from frappe/version-16-hotfix 2026-06-10 05:53:43 +05:30
Frappe PR Bot
6bcab7cfc8 chore(release): Bumped to Version 16.21.1
## [16.21.1](https://github.com/frappe/erpnext/compare/v16.21.0...v16.21.1) (2026-06-03)

### Bug Fixes

* item report view ([656d1bd](656d1bd6e3))
2026-06-03 09:43:23 +00:00
Nishka Gosalia
39c8161011 Merge pull request #55593 from frappe/mergify/bp/version-16/pr-55592
fix: item report view (backport #55591) (backport #55592)
2026-06-03 15:11:44 +05:30
nishkagosalia
656d1bd6e3 fix: item report view
(cherry picked from commit bca917380d)
(cherry picked from commit 7dfae51044)
2026-06-03 09:39:24 +00:00
Frappe PR Bot
ecb572de92 chore(release): Bumped to Version 16.21.0
# [16.21.0](https://github.com/frappe/erpnext/compare/v16.20.1...v16.21.0) (2026-06-02)

### Bug Fixes

* asset scrap flow related changes ([e3f03a2](e3f03a21c3))
* billing address does not belongs to the company error ([e1f29de](e1f29de078))
* **book_appointment:** when scheduling is disabled, block API endpoints (backport [#55455](https://github.com/frappe/erpnext/issues/55455)) ([#55457](https://github.com/frappe/erpnext/issues/55457)) ([aa5dfde](aa5dfde23b))
* changes as per review ([8b1d981](8b1d9817a6))
* check perm for account (backport [#55479](https://github.com/frappe/erpnext/issues/55479)) ([#55483](https://github.com/frappe/erpnext/issues/55483)) ([0c946f2](0c946f2420))
* **custom_financial_template:** sum account closing balances across dimensions ([3359e20](3359e20d06))
* import DateTimeLikeObject ([d82e03e](d82e03edb6))
* **issue:** check permission before issue status modification (backport [#55458](https://github.com/frappe/erpnext/issues/55458)) ([#55460](https://github.com/frappe/erpnext/issues/55460)) ([7c5d617](7c5d617049))
* item master list view UI cleanup ([2d554c0](2d554c05d6))
* **je:** preserve account on duplicate row when party row exists (backport [#55180](https://github.com/frappe/erpnext/issues/55180)) ([#55512](https://github.com/frappe/erpnext/issues/55512)) ([fe585dc](fe585dc225))
* Make Distributed Discount Amount field read only ([29441b7](29441b7249))
* **manufacturing:** allow to edit batch size while creating a work order (backport [#55058](https://github.com/frappe/erpnext/issues/55058)) ([#55326](https://github.com/frappe/erpnext/issues/55326)) ([ae92a82](ae92a82930))
* material transfer in transit issue ([356bb78](356bb7878f))
* merge conflicts ([b74e365](b74e365421))
* new bom version should not recalculate operations through routing (backport [#55370](https://github.com/frappe/erpnext/issues/55370)) ([#55372](https://github.com/frappe/erpnext/issues/55372)) ([933ac01](933ac0108c))
* over order allowance setting fix ([41b2de3](41b2de35a9))
* pick correct name when creating user from RFQ (backport [#55468](https://github.com/frappe/erpnext/issues/55468)) ([#55472](https://github.com/frappe/erpnext/issues/55472)) ([fc842fb](fc842fb45f))
* **pos:** escape html output in pos page templates (backport [#55527](https://github.com/frappe/erpnext/issues/55527)) ([#55529](https://github.com/frappe/erpnext/issues/55529)) ([224426e](224426e06b))
* **pos:** escape item data on pos item selector (backport [#55503](https://github.com/frappe/erpnext/issues/55503)) ([#55508](https://github.com/frappe/erpnext/issues/55508)) ([5393c93](5393c93675))
* **pos:** preserve contacts and enforce permissions in set_customer_info (backport [#55463](https://github.com/frappe/erpnext/issues/55463)) ([#55466](https://github.com/frappe/erpnext/issues/55466)) ([ef2700b](ef2700bec6))
* **ppr:** make default_advance_account optional ([7a7cc31](7a7cc31523))
* **quotation:** made customer contact column visible (backport [#55433](https://github.com/frappe/erpnext/issues/55433)) ([#55435](https://github.com/frappe/erpnext/issues/55435)) ([2feb8eb](2feb8eb370))
* **regional:** Japanese CT Rate (backport [#54998](https://github.com/frappe/erpnext/issues/54998)) ([#55438](https://github.com/frappe/erpnext/issues/55438)) ([7426aaf](7426aaf1e2))
* resolve conflict ([abe19e1](abe19e1212))
* **selling:** handle None values while grouping opportunities by utm … (backport [#55300](https://github.com/frappe/erpnext/issues/55300)) ([#55328](https://github.com/frappe/erpnext/issues/55328)) ([198970c](198970cdee))
* set a fallback value if no fiscal year set ([1521410](1521410125))
* stock reco for legacy serial nos ([67c922c](67c922cdf3))
* **stock:** add warning message to notify the user to configure the inspection ([1679680](1679680d8e))
* **stock:** allow to create quality inspection after purchase/delivery ([51a140a](51a140a2bd))
* **stock:** change qb to qb get_query to fix filter issues (backport [#55443](https://github.com/frappe/erpnext/issues/55443)) ([#55445](https://github.com/frappe/erpnext/issues/55445)) ([277a072](277a0723ef))
* **stock:** get_actual_qty during cancellations (backport [#55388](https://github.com/frappe/erpnext/issues/55388)) ([#55392](https://github.com/frappe/erpnext/issues/55392)) ([faa1573](faa15731cb))
* **tds:** treat NULL and empty-string tax_withholding_group as equivalent ([82e12d2](82e12d2d52))
* unable to submit subcontracted job card (backport [#55537](https://github.com/frappe/erpnext/issues/55537)) ([#55540](https://github.com/frappe/erpnext/issues/55540)) ([ceb1042](ceb10422ae))
* update default_advance_account type ([0bbc493](0bbc493213))
* use fiscal year instead of calendar year in accounting dashboard number cards ([81d10d3](81d10d32f2))
* use get_query instead of get_all for data fetching ([7cbef15](7cbef15596))
* **UX:** Move title field to More Info ([20592fc](20592fc25d))

### Features

* build and upload assets to GitHub Releases ([4c05ebc](4c05ebc21e))
* over order allowance setting ([08eaaa5](08eaaa5b83))
* **payment-entry:** warn user before cancelling reconciled payment entry ([61d6d2f](61d6d2f344))
2026-06-02 16:55:08 +00:00
Mihir Kandoi
23181b3962 Merge pull request #55547 from frappe/version-16-hotfix 2026-06-02 22:23:38 +05:30
ruthra kumar
4af265c48f Merge pull request #55502 from frappe/mergify/bp/version-16/pr-55495
fix: opening bal double counting in Process Period Closing Voucher (backport #55495)
2026-06-01 16:00:41 +05:30
ruthra kumar
ce97a74c5f test: prevent double counting of opening balances
(cherry picked from commit 7f2af123ee)
2026-06-01 09:36:47 +00:00
ruthra kumar
e955b4a3b9 refactor: color coded status in list view
(cherry picked from commit cfeffbb354)
2026-06-01 09:36:46 +00:00
ruthra kumar
de42a9e86e refactor: tabbed view for process period closing voucher
(cherry picked from commit 1960c81619)
2026-06-01 09:36:46 +00:00
ruthra kumar
5206b279b6 refactor: only consider non-opening balance for Balance sheet accounts
(cherry picked from commit a2b8334046)
2026-06-01 09:36:46 +00:00
Frappe PR Bot
9af618d6bf chore(release): Bumped to Version 16.20.1
## [16.20.1](https://github.com/frappe/erpnext/compare/v16.20.0...v16.20.1) (2026-06-01)

### Bug Fixes

* billing address does not belongs to the company error ([d6b7791](d6b7791f18))
2026-06-01 06:08:21 +00:00
rohitwaghchaure
3c3cde4362 Merge pull request #55475 from frappe/mergify/bp/version-16/pr-55425
fix: billing address does not belongs to the company error (backport #55417) (backport #55425)
2026-06-01 11:36:44 +05:30
Rohit Waghchaure
d6b7791f18 fix: billing address does not belongs to the company error
(cherry picked from commit 9df07b367a)
(cherry picked from commit e1f29de078)
2026-06-01 06:02:05 +00:00
Frappe PR Bot
ff46d20b25 chore(release): Bumped to Version 16.20.0
# [16.20.0](https://github.com/frappe/erpnext/compare/v16.19.1...v16.20.0) (2026-05-27)

### Bug Fixes

* consider batchwise valuation in stock ageing report (backport [#54919](https://github.com/frappe/erpnext/issues/54919)) ([#55230](https://github.com/frappe/erpnext/issues/55230)) ([a1457c7](a1457c759d))
* consumed operation cost calculation (backport [#54858](https://github.com/frappe/erpnext/issues/54858)) ([#55133](https://github.com/frappe/erpnext/issues/55133)) ([dfc9144](dfc91441b4))
* correct description for Is Rate Adjustment Entry (Debit Note) checkbox ([a39733d](a39733ddd0))
* correct remarks for foreign currency payment entries ([f3f8f32](f3f8f327df))
* corrected the pricing rule taking the wrong value (backport [#54894](https://github.com/frappe/erpnext/issues/54894)) ([#55124](https://github.com/frappe/erpnext/issues/55124)) ([1a75c14](1a75c14308))
* default use_for_shopping_cart to 0 in set_taxes ([54be4ee](54be4ee275))
* don't reset net_purchase_amount for Composite Asset if already set ([99642b9](99642b9636))
* edit stock uom qty for purchase documents (backport [#55135](https://github.com/frappe/erpnext/issues/55135)) ([#55179](https://github.com/frappe/erpnext/issues/55179)) ([123b4ad](123b4ad563))
* **employee:** js error if user does not have write permission for date field (backport [#55312](https://github.com/frappe/erpnext/issues/55312)) ([#55314](https://github.com/frappe/erpnext/issues/55314)) ([4dff5a7](4dff5a7820))
* faster range calculation on process period closing voucher ([e56ee38](e56ee383bc))
* fg valuation rate in repack entry when multiple FGs ([7b6adce](7b6adce89a))
* inclusive tax amount not considered while setting LCV from purchase invoice ([bd4c244](bd4c24493c))
* incorrect error message string in sales order (backport [#55090](https://github.com/frappe/erpnext/issues/55090)) ([#55095](https://github.com/frappe/erpnext/issues/55095)) ([17bc2b6](17bc2b691f))
* invalid filter on item_group (backport [#55186](https://github.com/frappe/erpnext/issues/55186)) ([#55188](https://github.com/frappe/erpnext/issues/55188)) ([ea86347](ea863477a4))
* item price with party condition (backport [#55100](https://github.com/frappe/erpnext/issues/55100)) ([#55107](https://github.com/frappe/erpnext/issues/55107)) ([cc438a4](cc438a4600))
* job card buttons color (backport [#55252](https://github.com/frappe/erpnext/issues/55252)) ([#55261](https://github.com/frappe/erpnext/issues/55261)) ([69c6ed3](69c6ed3cd9))
* **manufacturing:** fetch from_bom name in production plan (backport [#55085](https://github.com/frappe/erpnext/issues/55085)) ([#55092](https://github.com/frappe/erpnext/issues/55092)) ([36aca51](36aca51fbb))
* **manufacturing:** remove forecast_qty and adjust_qty fields from sa… (backport [#55129](https://github.com/frappe/erpnext/issues/55129)) ([#55136](https://github.com/frappe/erpnext/issues/55136)) ([bde7f16](bde7f1660e))
* **payment_entry:** sync paid/received amounts for cross-currency entries (backport [#55270](https://github.com/frappe/erpnext/issues/55270)) ([#55272](https://github.com/frappe/erpnext/issues/55272)) ([705814f](705814f066))
* pos profile form cleanup (backport [#52436](https://github.com/frappe/erpnext/issues/52436)) ([#55285](https://github.com/frappe/erpnext/issues/55285)) ([1f14ef2](1f14ef2344))
* prevent AttributeError in batch query filters (backport [#55257](https://github.com/frappe/erpnext/issues/55257)) ([#55279](https://github.com/frappe/erpnext/issues/55279)) ([bfd37dc](bfd37dcc21))
* **project:** update customer and sales order as no copy ([1e61ca1](1e61ca162f))
* removed redundant code ([a7eb3ac](a7eb3acd1a))
* **sales_invoice:** skip stock update for POS invoices linked to Delivery Note (backport [#55311](https://github.com/frappe/erpnext/issues/55311)) ([#55313](https://github.com/frappe/erpnext/issues/55313)) ([cd7e1bb](cd7e1bbff1))
* set bin details when adding item using update items (backport [#55096](https://github.com/frappe/erpnext/issues/55096)) ([#55098](https://github.com/frappe/erpnext/issues/55098)) ([bb87ffc](bb87ffc90a))
* single variant creation error ([82b0372](82b0372d5b))
* slow query ([66c9170](66c9170465))
* **stock:** apply posting datetime filters while fetching available batches (backport [#54976](https://github.com/frappe/erpnext/issues/54976)) ([#55185](https://github.com/frappe/erpnext/issues/55185)) ([edf6bea](edf6bea2ee))
* **stock:** remove precision for valuation rate while creating sle (backport [#55249](https://github.com/frappe/erpnext/issues/55249)) ([#55260](https://github.com/frappe/erpnext/issues/55260)) ([9600ecd](9600ecd61c))
* **stock:** remove recalculate current qty function (backport [#54774](https://github.com/frappe/erpnext/issues/54774)) ([#55075](https://github.com/frappe/erpnext/issues/55075)) ([56a9b37](56a9b37fac))
* use passed posting date in make_reverse_gl_entries ([3ce9cf2](3ce9cf2bd8))
* valuation rate missing for standalone credit notes for moving av… (backport [#55102](https://github.com/frappe/erpnext/issues/55102)) ([#55104](https://github.com/frappe/erpnext/issues/55104)) ([b11365b](b11365b8c2))

### Features

* add get_parent_supplier_groups using query builder ([82793cb](82793cbd4d))
* add party groups functionality to party specific item (backport [#54988](https://github.com/frappe/erpnext/issues/54988)) ([#55245](https://github.com/frappe/erpnext/issues/55245)) ([a618f4c](a618f4cca4))
* allow creation of any number of variants in multiple item variant creation dialog ([27db98d](27db98d222))
* pending qty in job card ([b372e6f](b372e6f118))

### Performance Improvements

* skip delink_original_entry during cancellation when Immutable Ledger is enabled ([#55130](https://github.com/frappe/erpnext/issues/55130)) ([8a4cb28](8a4cb28d90))
* skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport [#55130](https://github.com/frappe/erpnext/issues/55130)) ([#55166](https://github.com/frappe/erpnext/issues/55166)) ([92689e0](92689e05da))
2026-05-27 01:22:47 +00:00
mergify[bot]
d215fa7623 chore: remove frappe-semgrep-rules submodule (backport #55083) (#55319)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-27 01:21:08 +00:00
Diptanil Saha
bf8f7ba883 Merge pull request #55307 from frappe/version-16-hotfix
chore: release v16
2026-05-27 05:33:15 +05:30
Frappe PR Bot
6ef4a2d82c chore(release): Bumped to Version 16.19.1
## [16.19.1](https://github.com/frappe/erpnext/compare/v16.19.0...v16.19.1) (2026-05-20)

### Bug Fixes

* faster range calculation on process period closing voucher ([fa4aa0c](fa4aa0c1b6))
2026-05-20 07:25:32 +00:00
ruthra kumar
e4e796146d Merge pull request #55082 from frappe/mergify/bp/version-16/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:53:58 +05:30
ruthra kumar
ea1d0cc277 refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:51:01 +00:00
ruthra kumar
fa4aa0c1b6 fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:51:01 +00:00
Frappe PR Bot
fdfcbf72bd chore(release): Bumped to Version 16.19.0
# [16.19.0](https://github.com/frappe/erpnext/compare/v16.18.3...v16.19.0) (2026-05-20)

### Bug Fixes

* add filter subtitle in print formats ([c4037da](c4037daca8))
* add warehouse vaildation for repack entry (backport [#54866](https://github.com/frappe/erpnext/issues/54866)) ([#54901](https://github.com/frappe/erpnext/issues/54901)) ([596c257](596c2571f6))
* changes to gl print template ([caa524f](caa524f661))
* disallow editing on reversal journals ([6a53982](6a53982f4a))
* **general-ledger:** show raw GL entries when categorize_by is empty (backport [#54816](https://github.com/frappe/erpnext/issues/54816)) ([#54830](https://github.com/frappe/erpnext/issues/54830)) ([c041cd2](c041cd27b5))
* handle None delivery_date when sorting MPS data (backport [#55028](https://github.com/frappe/erpnext/issues/55028)) ([#55059](https://github.com/frappe/erpnext/issues/55059)) ([f272d32](f272d32f80))
* improve design and refactor ar print template ([059372a](059372add5))
* improve filter details render logic to avoid showing duplicate information ([040b31d](040b31d3a7))
* incoming rate for legacy serial no (backport [#54962](https://github.com/frappe/erpnext/issues/54962)) ([#54978](https://github.com/frappe/erpnext/issues/54978)) ([6bce78c](6bce78c66d))
* minor bug fixes for ar print template ([09b19f7](09b19f7a2a))
* minor bugs in print templates ([e1446fc](e1446fc6f4))
* minor changes in print template ([0ead229](0ead2296e6))
* minor changes in print template ([16bc28b](16bc28bd70))
* minor changes in print templates ([0d50e03](0d50e03595))
* minor text issues in print ([daaa4ca](daaa4ca0c8))
* normalize date comparison to avoid datatype mismatch ([42f6cb4](42f6cb40d1))
* **patch:** drop dead procedures first before other changes ([0df9591](0df9591910))
* **payment_entry:** fix paid/received amount calculation for multi-currency accounts (backport [#54963](https://github.com/frappe/erpnext/issues/54963)) ([#54970](https://github.com/frappe/erpnext/issues/54970)) ([48b09eb](48b09eb52e))
* posting date and time ([1c44c60](1c44c60dbd))
* prevent duplicate task execution and timestamp error in transaction deletion (backport [#55021](https://github.com/frappe/erpnext/issues/55021)) ([#55025](https://github.com/frappe/erpnext/issues/55025)) ([9857cc6](9857cc64d6))
* remove parent page ([10b4090](10b409005d))
* remove sql procedure method from AR report ([414319d](414319daeb))
* revamp print formats for accounts receivable summary and accounts payable summary reports ([928fab6](928fab6f7e))
* status not changing for dropshipped POs and SOs (backport [#54934](https://github.com/frappe/erpnext/issues/54934)) ([#54937](https://github.com/frappe/erpnext/issues/54937)) ([3c571a1](3c571a1691))
* stock balance showing incorrect value because of incorrect SLE ([0b3344b](0b3344bad9))
* **stock:** add whole number quantity validation in Stock Reconciliation (backport [#54922](https://github.com/frappe/erpnext/issues/54922)) ([#54925](https://github.com/frappe/erpnext/issues/54925)) ([c499454](c4994548c3))
* **stock:** ignore fetching warehouse account for asset items (backport [#54403](https://github.com/frappe/erpnext/issues/54403)) ([#54961](https://github.com/frappe/erpnext/issues/54961)) ([5e5b5cf](5e5b5cfa0c))
* **stock:** update buying amount calculation in gross profit report (backport [#55020](https://github.com/frappe/erpnext/issues/55020)) ([#55024](https://github.com/frappe/erpnext/issues/55024)) ([8870619](88706192d7))
* styling in trial_balance.html and print format ([9a18d31](9a18d318d9))
* toast message for item price insert ([#55009](https://github.com/frappe/erpnext/issues/55009)) ([c967792](c967792ccb))
* use route_options for Credit Note and Debit Note sidebar links (backport [#55026](https://github.com/frappe/erpnext/issues/55026)) ([#55063](https://github.com/frappe/erpnext/issues/55063)) ([1941c3b](1941c3b136))
* **UX:** Buying settings form cleanup ([#54731](https://github.com/frappe/erpnext/issues/54731)) ([e7ae296](e7ae296614))
* **UX:** Item master form cleanup ([#54538](https://github.com/frappe/erpnext/issues/54538)) ([0eb049c](0eb049cd85))
* validate company region in uae vat 201 (backport [#54899](https://github.com/frappe/erpnext/issues/54899)) ([#55055](https://github.com/frappe/erpnext/issues/55055)) ([4015c2b](4015c2b9a4))
* warn when accounting dimension fieldname conflicts with existing fields (backport [#55036](https://github.com/frappe/erpnext/issues/55036)) ([#55062](https://github.com/frappe/erpnext/issues/55062)) ([68a5eae](68a5eae3ff))

### Features

* add print format for accounts payable report ([1c6dc80](1c6dc80b70))
* introduce print format for Accounts Receivable report ([4e7f2ee](4e7f2eeaa0))
* introduce print formats for financial statements ([3283c46](3283c461f1))
* print format for report trial balance ([1d08448](1d08448d1a))

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) ([#54908](https://github.com/frappe/erpnext/issues/54908)) ([0d07083](0d07083299))
2026-05-20 04:10:54 +00:00
diptanilsaha
fb7f820885 Merge pull request #55051 from frappe/version-16-hotfix
chore: release v16
2026-05-20 09:39:15 +05:30
ruthra kumar
799d6d159c Merge pull request #54960 from frappe/mergify/bp/version-16/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 14:31:57 +05:30
ruthra kumar
48f59a033f refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 07:32:52 +00:00
Frappe PR Bot
2807c9f08f chore(release): Bumped to Version 16.18.3
## [16.18.3](https://github.com/frappe/erpnext/compare/v16.18.2...v16.18.3) (2026-05-14)

### Bug Fixes

* status not changing for dropshipped POs and SOs (backport [#54934](https://github.com/frappe/erpnext/issues/54934)) (backport [#54937](https://github.com/frappe/erpnext/issues/54937)) ([#54938](https://github.com/frappe/erpnext/issues/54938)) ([5271773](5271773595))
2026-05-14 09:39:02 +00:00
mergify[bot]
5271773595 fix: status not changing for dropshipped POs and SOs (backport #54934) (backport #54937) (#54938)
fix: status not changing for dropshipped POs and SOs (backport #54934) (#54937)

fix: status not changing for dropshipped POs and SOs (#54934)

* fix: status not changing for dropshipped POs and SOs

* test: change test case to accomodate new flow

(cherry picked from commit 78a79120ea)


(cherry picked from commit 3c571a1691)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-14 09:37:26 +00:00
Frappe PR Bot
dd35cd1f84 chore(release): Bumped to Version 16.18.2
## [16.18.2](https://github.com/frappe/erpnext/compare/v16.18.1...v16.18.2) (2026-05-14)

### Bug Fixes

* posting date and time ([ab09029](ab090295d9))
2026-05-14 05:35:39 +00:00
rohitwaghchaure
77a6299e8b Merge pull request #54931 from frappe/mergify/bp/version-16/pr-54928
fix: posting date and time (backport #54905) (backport #54928)
2026-05-14 11:04:07 +05:30
rohitwaghchaure
b79ec7cbdd chore: fix linter issue
(cherry picked from commit 3c993377aa)
(cherry picked from commit 21ada7799c)
2026-05-14 02:04:52 +00:00
rohitwaghchaure
927360dd1d chore: fixed test case
(cherry picked from commit c740f77a6f)
(cherry picked from commit f4e66914c6)
2026-05-14 02:04:52 +00:00
Rohit Waghchaure
ab090295d9 fix: posting date and time
(cherry picked from commit fb6c05f186)
(cherry picked from commit 1c44c60dbd)
2026-05-14 02:04:51 +00:00
Frappe PR Bot
c4b7b15824 chore(release): Bumped to Version 16.18.1
## [16.18.1](https://github.com/frappe/erpnext/compare/v16.18.0...v16.18.1) (2026-05-13)

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) (backport [#54908](https://github.com/frappe/erpnext/issues/54908)) ([#54916](https://github.com/frappe/erpnext/issues/54916)) ([cfd3847](cfd3847255))
2026-05-13 10:16:21 +00:00
mergify[bot]
cfd3847255 Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (backport #54908) (#54916)
Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54908)

Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)


(cherry picked from commit 0d07083299)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 10:14:46 +00:00
Frappe PR Bot
dc914adb62 chore(release): Bumped to Version 16.18.0
# [16.18.0](https://github.com/frappe/erpnext/compare/v16.17.0...v16.18.0) (2026-05-12)

### Bug Fixes

* added permission validation for `deactivate_sales_person` (backport [#54884](https://github.com/frappe/erpnext/issues/54884)) ([#54886](https://github.com/frappe/erpnext/issues/54886)) ([98de025](98de025a09))
* check if item is dropshipped before updating quantity (backport [#54825](https://github.com/frappe/erpnext/issues/54825)) ([#54827](https://github.com/frappe/erpnext/issues/54827)) ([0db7e1e](0db7e1e56b))
* **crm:** handle empty _assign in appointment auto assignment (backport [#54782](https://github.com/frappe/erpnext/issues/54782)) ([#54795](https://github.com/frappe/erpnext/issues/54795)) ([f36bdaa](f36bdaadae))
* decimal issue ([8b9b83a](8b9b83a9df))
* do not rely on client side to update quantities during partial d… (backport [#54804](https://github.com/frappe/erpnext/issues/54804)) ([#54821](https://github.com/frappe/erpnext/issues/54821)) ([f24b556](f24b556336))
* fetch get_item_tax_template while update items (backport [#53708](https://github.com/frappe/erpnext/issues/53708)) ([#54767](https://github.com/frappe/erpnext/issues/54767)) ([4fbaea1](4fbaea17f8))
* incorrect serial nos picked during disassemble (backport [#54757](https://github.com/frappe/erpnext/issues/54757)) ([#54760](https://github.com/frappe/erpnext/issues/54760)) ([66ae590](66ae590adc))
* incorrect validation thrown for drop shipped PI (backport [#54751](https://github.com/frappe/erpnext/issues/54751)) ([#54753](https://github.com/frappe/erpnext/issues/54753)) ([379ebbe](379ebbe8c4))
* raw material should not have target warehouse in manufacture entry (backport [#54849](https://github.com/frappe/erpnext/issues/54849)) ([#54861](https://github.com/frappe/erpnext/issues/54861)) ([3dbadfa](3dbadfadd5))
* rename supplier wise stock analytics report ([7086db1](7086db1e1c))
* **stock:** apply filters for rejected warehouse in pick list (backport [#54733](https://github.com/frappe/erpnext/issues/54733)) ([#54776](https://github.com/frappe/erpnext/issues/54776)) ([cf0d9df](cf0d9dfbfd))
* **stock:** ignore reserved qty for stock levels in batch (backport [#54790](https://github.com/frappe/erpnext/issues/54790)) ([#54797](https://github.com/frappe/erpnext/issues/54797)) ([338d190](338d1904c1))
* **stock:** priorities pick list parent warehouse (backport [#54788](https://github.com/frappe/erpnext/issues/54788)) ([#54793](https://github.com/frappe/erpnext/issues/54793)) ([d3bc629](d3bc629f68))
* **task:** update depends_on for closing date and review date [#54850](https://github.com/frappe/erpnext/issues/54850) (backport [#54852](https://github.com/frappe/erpnext/issues/54852)) ([#54863](https://github.com/frappe/erpnext/issues/54863)) ([b962a1a](b962a1a0cd))
* validate variant values (backport [#54831](https://github.com/frappe/erpnext/issues/54831)) ([#54839](https://github.com/frappe/erpnext/issues/54839)) ([87b798b](87b798b936))

### Features

* partial delivery in dropshipping (backport [#54787](https://github.com/frappe/erpnext/issues/54787)) ([#54800](https://github.com/frappe/erpnext/issues/54800)) ([f64f871](f64f871d45))
* Philippines chart of account (backport [#53918](https://github.com/frappe/erpnext/issues/53918)) ([#54888](https://github.com/frappe/erpnext/issues/54888)) ([8f03108](8f0310859d))
2026-05-12 18:49:27 +00:00
diptanilsaha
41bff45d7a Merge pull request #54865 from frappe/version-16-hotfix
chore: release v16
2026-05-13 00:17:54 +05:30
Frappe PR Bot
7b494dc9e8 chore(release): Bumped to Version 16.17.0
# [16.17.0](https://github.com/frappe/erpnext/compare/v16.16.0...v16.17.0) (2026-05-05)

### Bug Fixes

* accounts and account types in German CoA "SKR 03" (backport [#54711](https://github.com/frappe/erpnext/issues/54711)) ([#54713](https://github.com/frappe/erpnext/issues/54713)) ([982810a](982810a700))
* add missing fields in set_currency_labels (backport [#54689](https://github.com/frappe/erpnext/issues/54689)) ([#54690](https://github.com/frappe/erpnext/issues/54690)) ([bca893a](bca893a508))
* Backfill `not_applicable` on Item Tax Template Details for German companies (backport [#54682](https://github.com/frappe/erpnext/issues/54682)) ([#54686](https://github.com/frappe/erpnext/issues/54686)) ([a22d773](a22d773341))
* copy project from first row to new rows (backport [#53295](https://github.com/frappe/erpnext/issues/53295)) ([#54620](https://github.com/frappe/erpnext/issues/54620)) ([e24ab72](e24ab72c0d))
* correct project filter in buying doctypes (backport [#54644](https://github.com/frappe/erpnext/issues/54644)) ([#54652](https://github.com/frappe/erpnext/issues/54652)) ([86cf256](86cf256358))
* correct titles set to {customer_name} or {supplier_name} text strings (backport [#54656](https://github.com/frappe/erpnext/issues/54656)) ([#54669](https://github.com/frappe/erpnext/issues/54669)) ([38cfeb1](38cfeb1bb7))
* dont show serial/batch button when PR is submitted (backport [#54642](https://github.com/frappe/erpnext/issues/54642)) ([#54646](https://github.com/frappe/erpnext/issues/54646)) ([6dbc17d](6dbc17d71a))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54725](https://github.com/frappe/erpnext/issues/54725)) ([2cd4c1a](2cd4c1a052))
* hide payment and payment request buttons based on permissions in invoices and orders (backport [#53920](https://github.com/frappe/erpnext/issues/53920)) ([#54736](https://github.com/frappe/erpnext/issues/54736)) ([e60490d](e60490dceb))
* incorrect expense account book in purchase return (backport [#54681](https://github.com/frappe/erpnext/issues/54681)) ([#54693](https://github.com/frappe/erpnext/issues/54693)) ([0dade2c](0dade2c38c))
* mark item tax templates as not applicable (backport [#54673](https://github.com/frappe/erpnext/issues/54673)) ([#54677](https://github.com/frappe/erpnext/issues/54677)) ([126e13b](126e13be25))
* **payment_entry:** convert the date args to string type before escaping in `get_outstanding_reference_documents` (backport [#54639](https://github.com/frappe/erpnext/issues/54639)) ([#54648](https://github.com/frappe/erpnext/issues/54648)) ([19a8ebe](19a8ebe8a5))
* **project:** use user.email for invitations and skip disabled users. (backport [#54561](https://github.com/frappe/erpnext/issues/54561)) ([#54667](https://github.com/frappe/erpnext/issues/54667)) ([288cdf3](288cdf3bf0))
* py error on sales forecast doctype (backport [#54641](https://github.com/frappe/erpnext/issues/54641)) ([#54643](https://github.com/frappe/erpnext/issues/54643)) ([7bd360a](7bd360aa29))
* Remove bom stock report link from manufacturing workspace ([0f27881](0f27881fed))
* **selling:** blanket order ordered qty recalculation on sales order status change (backport [#54593](https://github.com/frappe/erpnext/issues/54593)) ([#54623](https://github.com/frappe/erpnext/issues/54623)) ([9db03bc](9db03bc520))
* set valid_from in created Item Price (backport [#54696](https://github.com/frappe/erpnext/issues/54696)) ([#54700](https://github.com/frappe/erpnext/issues/54700)) ([bbb4e79](bbb4e79d0a))
* show correct status in Serial No Ledger (backport [#54567](https://github.com/frappe/erpnext/issues/54567)) ([#54626](https://github.com/frappe/erpnext/issues/54626)) ([d6f2ff6](d6f2ff6b87))
* show in and out qty in the stock ledger report for stock recos ([d27cf48](d27cf48b19))
* skip depreciation rescheduling when asset is fully depreciated on sale ([d3c893d](d3c893d08b))
* skip rescheduling only for asset being disposed ([07a957c](07a957c164))
* use RecoverableErrors isinstance check for repost timeout status (backport [#54543](https://github.com/frappe/erpnext/issues/54543)) ([#54649](https://github.com/frappe/erpnext/issues/54649)) ([b300159](b3001595ab))

### Features

* copy terms attachments to transactions (backport [#53403](https://github.com/frappe/erpnext/issues/53403)) ([#54661](https://github.com/frappe/erpnext/issues/54661)) ([bd932da](bd932da08b))
* **ux:** Naming series dialog ([#54554](https://github.com/frappe/erpnext/issues/54554)) ([48ebb4c](48ebb4ca61))

### Performance Improvements

* max recursion depth error in serial no (backport [#54629](https://github.com/frappe/erpnext/issues/54629)) ([#54631](https://github.com/frappe/erpnext/issues/54631)) ([808214f](808214fd95))
2026-05-05 16:32:20 +00:00
diptanilsaha
ed69dafbe8 Merge pull request #54740 from frappe/version-16-hotfix 2026-05-05 22:00:39 +05:30
Frappe PR Bot
4d5c665e22 chore(release): Bumped to Version 16.16.0
# [16.16.0](https://github.com/frappe/erpnext/compare/v16.15.1...v16.16.0) (2026-04-28)

### Bug Fixes

* **`get_stock_balance`:** validate inventory dimension fieldnames (backport [#54587](https://github.com/frappe/erpnext/issues/54587)) ([#54589](https://github.com/frappe/erpnext/issues/54589)) ([9f04fcc](9f04fcc190))
* add filter labels and required filters for financial report validation ([e6f0bb6](e6f0bb66e2))
* add party type for dynamic link support ([c6d4802](c6d4802857))
* always exclude pcv entries except for closing account head ([446c111](446c111653))
* avoid double reduction of pe reference outstanding (backport [#54193](https://github.com/frappe/erpnext/issues/54193)) ([#54613](https://github.com/frappe/erpnext/issues/54613)) ([5de4b01](5de4b013ea))
* correct display depends on condition ([#54556](https://github.com/frappe/erpnext/issues/54556)) ([0df38a8](0df38a841e))
* debit credit not equal in purchase transactions for multi currency (backport [#54456](https://github.com/frappe/erpnext/issues/54456)) ([#54564](https://github.com/frappe/erpnext/issues/54564)) ([d9a9a5b](d9a9a5bcde))
* delivery schedule in the sales order ([386f499](386f49978e))
* duplicate entries being shown in batch exists in future transact… (backport [#54604](https://github.com/frappe/erpnext/issues/54604)) ([#54606](https://github.com/frappe/erpnext/issues/54606)) ([1111771](11117710d3))
* **edi:** restrict Code List imports to files and trusted backend URLs (backport [#54137](https://github.com/frappe/erpnext/issues/54137)) ([#54266](https://github.com/frappe/erpnext/issues/54266)) ([2a244d1](2a244d162b)), closes [#54488](https://github.com/frappe/erpnext/issues/54488)
* ensure fiscal year is checked before validating date filters in financial statements ([fba7871](fba78711cc))
* ensure tax withholding entries respect date range of category ([719d982](719d982a07))
* filter opening entries in first year in custom financial statement ([6bd6e62](6bd6e62c8c))
* filter overdue purchase order items by company (backport [#54099](https://github.com/frappe/erpnext/issues/54099)) ([#54611](https://github.com/frappe/erpnext/issues/54611)) ([8f8bf13](8f8bf13b41))
* hide feature flag controlled fields on install ([45dc2c4](45dc2c40fd))
* make inv dimen reqd only in delivery note (backport [#54546](https://github.com/frappe/erpnext/issues/54546)) ([#54552](https://github.com/frappe/erpnext/issues/54552)) ([d56df96](d56df96f73))
* **manufacturing:** remove conversion factor for stock qty (backport [#54525](https://github.com/frappe/erpnext/issues/54525)) ([#54573](https://github.com/frappe/erpnext/issues/54573)) ([f14751d](f14751d538))
* negative quantity check in validate_item_qty (backport [#54559](https://github.com/frappe/erpnext/issues/54559)) ([#54572](https://github.com/frappe/erpnext/issues/54572)) ([f7fa394](f7fa394aea))
* **payment_entry:** escape arguments on invoice and order fetching sql queries (backport [#54582](https://github.com/frappe/erpnext/issues/54582)) ([#54586](https://github.com/frappe/erpnext/issues/54586)) ([5289aa0](5289aa0ab3))
* **PCV:** set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (backport [#54522](https://github.com/frappe/erpnext/issues/54522)) ([#54524](https://github.com/frappe/erpnext/issues/54524)) ([f3996fb](f3996fb971))
* preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440)) ([#54493](https://github.com/frappe/erpnext/issues/54493)) ([456e99b](456e99b352))
* py error on stock ageing report (backport [#54467](https://github.com/frappe/erpnext/issues/54467)) ([#54469](https://github.com/frappe/erpnext/issues/54469)) ([090aab3](090aab33fb))
* skip BudgetValidation when cancelling GL entries ([1b14673](1b146738c4))
* **stock:** add stock entry in batch master connection ([62bbe28](62bbe28a72))
* **stock:** remove validation for transfer_qty field (backport [#54542](https://github.com/frappe/erpnext/issues/54542)) ([#54545](https://github.com/frappe/erpnext/issues/54545)) ([cc85370](cc85370d54))
* **stock:** set incoming rate as zero for outward sle (backport [#54514](https://github.com/frappe/erpnext/issues/54514)) ([#54533](https://github.com/frappe/erpnext/issues/54533)) ([cabea2f](cabea2f288))
* **stock:** show available qty in warehouse link field (backport [#54474](https://github.com/frappe/erpnext/issues/54474)) ([#54484](https://github.com/frappe/erpnext/issues/54484)) ([f7b87ed](f7b87ed0e3))
* **stock:** show item code in serial and batch selector dialog ([85d1eb8](85d1eb8379))
* summing of values could be zero even if values exist ([d51ce66](d51ce66cb2))
* update account identification to avoid using name_field in financial statements ([7b60ec8](7b60ec8457))
* update fiscal year filter to use mandatory_depends_on instead of reqd ([6570796](6570796fba))
* update status of quotation in patch (backport [#54577](https://github.com/frappe/erpnext/issues/54577)) ([#54580](https://github.com/frappe/erpnext/issues/54580)) ([134e4b7](134e4b7446))

### Features

* add setting to hide Subscription references across doctypes ([#54576](https://github.com/frappe/erpnext/issues/54576)) ([15b6633](15b6633fc3))
* Add XLSX styling support to custom financial report templates ([#52612](https://github.com/frappe/erpnext/issues/52612)) ([055ff56](055ff56ce4))
* Add XLSX styling support to custom financial report templates (backport [#52612](https://github.com/frappe/erpnext/issues/52612)) ([#54485](https://github.com/frappe/erpnext/issues/54485)) ([df3fbed](df3fbeded2))
* danish_bosnian_address_template (backport [#54093](https://github.com/frappe/erpnext/issues/54093)) ([#54516](https://github.com/frappe/erpnext/issues/54516)) ([5c0d2cb](5c0d2cb474))
* enhance account category with root type ([#53190](https://github.com/frappe/erpnext/issues/53190)) ([96bab08](96bab08ae0))
2026-04-28 21:03:29 +00:00
diptanilsaha
e09487d140 Merge pull request #54583 from frappe/version-16-hotfix 2026-04-29 02:31:54 +05:30
105 changed files with 39092 additions and 165419 deletions

View File

@@ -1,72 +0,0 @@
#!/bin/bash
#
# Hydrate a test shard from the setup job's artifact.
#
# The bench (apps, venv, node_modules, sites) is already on disk at ~/frappe-bench — the
# workflow untar'd it from the artifact the setup job built. So there is NO bench init, no
# asset build, and no reinstall here: just bring the DB up on the baked datadir and start redis
# so tests can run. The whole point is that the expensive work happened ONCE in the setup job.
#
set -e
ci_user="${ERPNEXT_CI_USER:-frappe}"
db_host="${DB_HOST:-127.0.0.1}"
# Re-exec as the ci user (uid 1001) so bench/cache ownership matches the artifact, same as
# install.sh. The workflow untar'd as root with -p, so the files are already owned by ci.
if [ "$(id -u)" = "0" ] && [ "${SKIP_SYSTEM_SETUP:-0}" = "1" ] && [ "$ci_user" != "root" ]; then
exec su -m "$ci_user" -s /bin/bash -c \
"ERPNEXT_CI_USER='$ci_user' DB_HOST='$db_host' DB='${DB:-}' bash '$0'"
fi
cd ~/frappe-bench
# Start the DB on the datadir baked into the artifact. It's already populated (the setup job
# reinstalled into this very datadir), so there is NO restore — the server comes up on the
# existing files. This is what replaces the per-shard SQL replay.
bash ~/frappe-bench/start-db.sh
# Bring up redis (lightmode unit tests need cache + queue). In the self-hosted container we use the
# full `bench start` (web/workers too, like install.sh). On the bare GitHub Postgres shard
# `bench start` (honcho) lagged — it blocks the redis procs behind web/worker procs the lightmode
# suite never uses, so the wait below burned its full timeout (~4m). There, start the two redis
# instances directly: fast and deterministic.
if [ "${DB:-mariadb}" = "postgres" ]; then
# Start redis directly as daemons — reliable and persists across steps. Do NOT route it through
# `bench start`: honcho tears the whole process group down if any one Procfile proc dies on the
# bare shard, which took redis with it (redis @ 13000 refused in Run Tests). Keeping redis
# independent is what makes it survive. The web server (for PDF tests) is NOT started here — a
# backgrounded server doesn't survive into the next step; it's started inside the Run Tests step.
for conf in redis_cache redis_queue; do
[ -f ~/frappe-bench/config/$conf.conf ] && redis-server ~/frappe-bench/config/$conf.conf --daemonize yes
done
else
bench start >> ~/frappe-bench/bench_start.log 2>&1 &
fi
# Wait for redis, failing fast instead of silently burning minutes if it never comes up.
cfg=~/frappe-bench/sites/common_site_config.json
if [ -f "$cfg" ]; then
ports=$(python - "$cfg" <<'PY'
import json, re, sys
try:
cfg = json.load(open(sys.argv[1]))
except Exception:
sys.exit(0)
for key in ("redis_cache", "redis_queue"):
m = re.search(r":(\d+)", str(cfg.get(key, "")))
if m:
print(m.group(1))
PY
)
for port in $ports; do
up=0
for _ in $(seq 1 60); do
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then exec 3>&- 3<&-; up=1; break; fi
sleep 1
done
[ "$up" = "1" ] || { echo "redis did not come up on port $port"; exit 1; }
done
fi
echo "Hydrated: DB up on baked datadir, redis up — ready for tests."

View File

@@ -7,106 +7,21 @@ cd ~ || exit
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
db_host=${DB_HOST:-"127.0.0.1"}
db_user_host=${DB_USER_HOST:-"localhost"}
wkhtmltox_deb=${WKHTMLTOX_DEB:-"/tmp/wkhtmltox.deb"}
bench_cache_dir=${BENCH_CACHE_DIR:-}
run_as_ci_user_if_needed() {
if [ "$(id -u)" != "0" ] || [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ] || [ "${ERPNEXT_CI_NON_ROOT:-0}" = "1" ]; then
return
fi
local missing_packages=()
if ! command -v pkg-config >/dev/null 2>&1; then
missing_packages+=("pkg-config")
fi
if ! command -v mariadb_config >/dev/null 2>&1 && ! command -v mysql_config >/dev/null 2>&1; then
missing_packages+=("libmariadb-dev")
fi
if ! command -v crontab >/dev/null 2>&1; then
missing_packages+=("cron")
fi
if [ "${#missing_packages[@]}" -gt 0 ]; then
apt-get update
apt-get install -y --no-install-recommends "${missing_packages[@]}"
fi
local ci_user="${ERPNEXT_CI_USER:-frappe}"
if ! id "$ci_user" >/dev/null 2>&1; then
useradd --home-dir "$HOME" --no-create-home --shell /bin/bash "$ci_user"
fi
rm -rf ~/frappe ~/frappe-bench
local ci_dirs=(
"$HOME"
"$GITHUB_WORKSPACE"
"$HOME/.cache"
"${PIP_CACHE_DIR:-$HOME/.cache/pip}"
"${npm_config_cache:-$HOME/.npm}"
"${YARN_CACHE_FOLDER:-$HOME/.cache/yarn}"
"$HOME/.yarn"
"${UV_CACHE_DIR:-$HOME/.cache/uv}"
"$(dirname "$wkhtmltox_deb")"
)
if [ -n "$bench_cache_dir" ]; then
ci_dirs+=("$bench_cache_dir")
fi
# Create + own (non-recursively) the home/cache/workspace dirs before dropping to
# the ci user. We deliberately do NOT wipe the yarn/uv caches here so a persistent
# cache (mounted volume or baked image layer) stays warm across runs.
mkdir -p "${ci_dirs[@]}" "$HOME/.yarn"
chown "$ci_user:$ci_user" "${ci_dirs[@]}" "$HOME/.yarn"
export ERPNEXT_CI_NON_ROOT=1
exec su -m "$ci_user" -s /bin/bash -c "cd '$HOME' && bash '$GITHUB_WORKSPACE/.github/helper/install.sh'"
}
run_as_ci_user_if_needed
run_ci_step() {
local label=$1
shift
echo "::group::${label}"
date -u
local exit_code=0
timeout --foreground "${CI_INSTALL_STEP_TIMEOUT:-1800}" "$@" || exit_code=$?
date -u
echo "::endgroup::"
return "$exit_code"
}
if [ -n "${GITHUB_WORKSPACE:-}" ]; then
git config --global --add safe.directory "$GITHUB_WORKSPACE" || true
git config --global --add safe.directory "$GITHUB_WORKSPACE/.git" || true
fi
rm -rf ~/frappe ~/frappe-bench
# ---------------------------------------------------------------------------
# Phase 1 — parallelise the three slow, independent setup steps:
# a) system packages b) frappe-bench pip install c) frappe git fetch
# ---------------------------------------------------------------------------
if [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ]; then
sudo apt-get update
sudo apt update
# apt remove/install must run sequentially but can overlap with pip and git.
sudo apt-get remove -y mysql-server mysql-client
sudo apt-get install -y libcups2-dev redis-server mariadb-client libmariadb-dev &
apt_pid=$!
# apt remove/install must run sequentially but can overlap with pip and git.
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev &
apt_pid=$!
pip install frappe-bench &
pip_pid=$!
else
apt_pid=
pip_pid=
fi
pip install frappe-bench &
pip_pid=$!
mkdir frappe
(
@@ -117,247 +32,76 @@ mkdir frappe
) &
clone_pid=$!
if [ -n "$apt_pid" ]; then wait $apt_pid; fi
if [ -n "$pip_pid" ]; then wait $pip_pid; fi
wait $apt_pid
wait $pip_pid
wait $clone_pid
pushd frappe
git checkout FETCH_HEAD
popd
frappe_sha=$(git -C frappe rev-parse HEAD)
get_bench_cache_archive() {
if [ -z "$bench_cache_dir" ]; then
return
fi
mkdir -p "$bench_cache_dir"
# Keyed on tool versions only (NOT the frappe SHA): any recent base bench works, because
# restore_warm_bench fast-forwards it to the exact live develop SHA. This is what lets a
# constantly-moving develop still hit the cache.
local cache_key
cache_key=$(
{
uname -m
python --version
node --version
bench --version
} | sha256sum | awk '{print $1}'
)
echo "${bench_cache_dir}/frappe-bench-base-${cache_key}.tar.zst"
}
restore_warm_bench() {
bench_cache_archive=$(get_bench_cache_archive)
[ -n "$bench_cache_archive" ] && [ -f "$bench_cache_archive" ] || return 1
echo "Restoring base bench from ${bench_cache_archive}"
tar --use-compress-program=unzstd -xf "$bench_cache_archive" -C ~ || return 1
[ -d ~/frappe-bench/apps/frappe/.git ] || return 1
mkdir -p ~/frappe-bench/sites ~/frappe-bench/logs
[ -f ~/frappe-bench/sites/apps.txt ] || printf "frappe\n" > ~/frappe-bench/sites/apps.txt
[ -f ~/frappe-bench/sites/common_site_config.json ] || printf "{}\n" > ~/frappe-bench/sites/common_site_config.json
# Fast-forward the restored frappe to the EXACT live develop SHA fetched in phase 1, then
# rebuild only what changed. The editable install means the venv tracks the new code with
# no reinstall. Any failure returns non-zero so the caller falls back to a full bench init.
if ! (
cd ~/frappe-bench/apps/frappe || exit 1
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
# straight from it (bench init names the remote 'upstream', not 'origin', and points
# it at this local clone — so a plain `git fetch origin` does not work).
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
git checkout --force FETCH_HEAD || exit 1
); then
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"
rm -rf ~/frappe-bench
return 1
fi
# Pick up any frappe dependency changes since the base was built (cached → fast if none),
# so a develop commit that bumped requirements doesn't leave a stale venv.
if ! ~/frappe-bench/env/bin/python -m pip install -q -e ~/frappe-bench/apps/frappe; then
echo "frappe dependency refresh failed; falling back to full init"
rm -rf ~/frappe-bench
return 1
fi
( cd ~/frappe-bench && CI=Yes bench build --app frappe ) || { rm -rf ~/frappe-bench; return 1; }
return 0
}
save_warm_bench() {
if [ -z "${bench_cache_archive:-}" ] || [ -f "$bench_cache_archive" ]; then
return
fi
if [ -n "$bench_cache_dir" ] && [ ! -w "$bench_cache_dir" ]; then
echo "Skipping warm bench save because ${bench_cache_dir} is not writable"
return
fi
local tmp_archive
tmp_archive="${bench_cache_archive}.${$}.tmp"
echo "Saving warm bench to ${bench_cache_archive}"
# Keep sites/common_site_config.json (the redis ports live there — dropping it makes the
# restore path fall back to a default redis port that bench start never bound, so reinstall
# fails with "redis ... connection refused"). Only the rebuildable sites/assets is excluded;
# restore_warm_bench runs `bench build` to regenerate it.
tar \
--use-compress-program="zstd -T0 -3" \
--exclude="frappe-bench/logs" \
--exclude="frappe-bench/sites/assets" \
-cf "$tmp_archive" \
-C ~ frappe-bench
mv "$tmp_archive" "$bench_cache_archive"
}
# ---------------------------------------------------------------------------
# Phase 2 — bench init and site setup
# ---------------------------------------------------------------------------
install_whktml() {
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
if [ ! -f "$wkhtmltox_deb" ]; then
wget -O "$wkhtmltox_deb" https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
fi
sudo apt-get install -y "$wkhtmltox_deb"
}
if [ "${SKIP_WKHTMLTOX_SETUP:-0}" != "1" ]; then
install_whktml &
wkpid=$!
else
wkpid=
fi
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
if ! restore_warm_bench; then
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
cd ~/frappe-bench || exit
sed -i 's/watch:/# watch:/g' Procfile
sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
CI=Yes bench build --app frappe
save_warm_bench
fi
if [ -n "$wkpid" ]; then wait $wkpid; fi
mkdir -p ~/frappe-bench/sites/test_site
mkdir ~/frappe-bench/sites/test_site
if [ "$DB" == "mariadb" ];then
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
if [ "$db_host" != "127.0.0.1" ]; then
sed -i "s/\"db_host\": \"127.0.0.1\"/\"db_host\": \"${db_host}\"/" ~/frappe-bench/sites/test_site/site_config.json
fi
else
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
fi
if [ "$DB" == "mariadb" ];then
for _ in {1..60}; do
if mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent; then
break
fi
sleep 1
done
mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
# Throwaway-DB durability tuning at runtime. (innodb_doublewrite is read-only on MariaDB
# 10.6, so it can't be disabled here — would need a server startup flag.)
mariadb --host "$db_host" --port 3306 -u root -proot \
# Belt-and-suspenders: also set performance variables at runtime in case
# MARIADB_EXTRA_FLAGS was not honoured by the container image.
mariadb --host 127.0.0.1 --port 3306 -u root -proot \
-e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0;"
# Opt-in DDL speedup: a shared tablespace avoids a create+fsync per DocType table during
# reinstall — a big win under disk contention. But ROW_FORMAT=DYNAMIC must be accepted in
# the system tablespace on this MariaDB. Enable with CI_INNODB_SHARED_TABLESPACE=1; if
# reinstall then errors on table creation, unset it (off by default — zero risk).
if [ "${CI_INNODB_SHARED_TABLESPACE:-0}" = "1" ]; then
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL innodb_file_per_table=0;"
fi
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'${db_user_host}' IDENTIFIED BY 'test_frappe'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host "$db_host" --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'${db_user_host}'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
# Disposable CI DB: durability off for speed (postgres fsyncs every commit by default, which
# dominates a commit-heavy suite). All reloadable, no restart. The postgres workflow runs a
# service-container DB and never calls start-db.sh, so the flags must be applied here.
echo "travis" | psql -h 127.0.0.1 -p 5432 -U postgres \
-c "ALTER SYSTEM SET synchronous_commit = 'off'" \
-c "ALTER SYSTEM SET fsync = 'off'" \
-c "ALTER SYSTEM SET full_page_writes = 'off'" \
-c "SELECT pg_reload_conf()";
fi
install_whktml() {
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
if [ ! -f /tmp/wkhtmltox.deb ]; then
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
fi
sudo apt install /tmp/wkhtmltox.deb
}
install_whktml &
wkpid=$!
cd ~/frappe-bench || exit
run_ci_step "Get payments app" bench get-app payments --branch develop
sed -i 's/watch:/# watch:/g' Procfile
sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
# Opt-in: skip building erpnext's frontend assets. Server tests don't need them, but PDF
# tests (print formats) do — they pass only if the PDF renderer ignores missing assets.
# Enable with CI_SKIP_ERPNEXT_ASSETS=1 to test; if PDF tests fail, unset it.
erpnext_get_app_args=()
if [ "${CI_SKIP_ERPNEXT_ASSETS:-0}" = "1" ]; then erpnext_get_app_args=(--skip-assets); fi
run_ci_step "Get erpnext app" bench get-app erpnext "${GITHUB_WORKSPACE}" "${erpnext_get_app_args[@]}"
bench get-app payments --branch develop
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then run_ci_step "Setup dev requirements" bench setup requirements --dev; fi
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench start >> ~/frappe-bench/bench_start.log 2>&1 &
wait $wkpid
# Under heavy concurrency, gunicorn's startup can delay redis coming up. reinstall and the
# tests need redis, so wait for it (best-effort, bounded) instead of racing — contention
# then slows the job rather than failing it.
wait_for_redis() {
local cfg=~/frappe-bench/sites/common_site_config.json
[ -f "$cfg" ] || return 0
local ports port
ports=$(python - "$cfg" <<'PY'
import json, re, sys
try:
cfg = json.load(open(sys.argv[1]))
except Exception:
sys.exit(0)
for key in ("redis_cache", "redis_queue"):
match = re.search(r":(\d+)", str(cfg.get(key, "")))
if match:
print(match.group(1))
PY
)
for port in $ports; do
local up=0
for _ in $(seq 1 120); do
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
exec 3>&- 3<&-; up=1
break
fi
sleep 1
done
# Fail clearly instead of letting reinstall die later on a vague socket-connection error
# when redis never bound.
[ "$up" = "1" ] || { echo "redis did not come up on port $port"; return 1; }
done
}
wait_for_redis
# Site setup: build the schema (~1000 DocTypes) into the DB. This is the single-threaded-Python
# bottleneck, but the fan-out amortises it — it runs once here in the setup job, and the test
# shards start the DB on the baked datadir instead of repeating the reinstall.
run_ci_step "Reinstall test site" bench --site test_site reinstall --yes
bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes

View File

@@ -1,79 +0,0 @@
#!/bin/bash
#
# Run MariaDB INSIDE the runner container, on a datadir we control. Because the datadir can be
# packaged into the bench artifact, test shards start an already-loaded server instead of
# replaying a SQL dump (the ~60s hydrate restore). Each shard gets its own copy → isolation kept.
#
# CI_DB_DATADIR picks the path:
# - setup job: /home/ci/db-data (OUTSIDE the bench, so install.sh's `rm -rf ~/frappe-bench`
# doesn't wipe it; it's moved into the bench just before packaging)
# - test shard: ~/frappe-bench/mariadb-data (where the artifact untar'd it)
#
# Idempotent: inits a fresh datadir if absent (setup), else starts on the existing one (shards).
#
set -e
ci_user="${ERPNEXT_CI_USER:-frappe}"
# Re-exec as the ci user so mariadbd and the datadir are owned consistently (root mariadbd is
# refused anyway). Mirrors install.sh's user switch.
if [ "$(id -u)" = "0" ] && [ "${SKIP_SYSTEM_SETUP:-0}" = "1" ] && [ "$ci_user" != "root" ]; then
exec su -m "$ci_user" -s /bin/bash -c \
"ERPNEXT_CI_USER='$ci_user' CI_DB_DATADIR='${CI_DB_DATADIR:-}' DB='${DB:-}' bash '$0'"
fi
# --- PostgreSQL (GitHub-hosted CI): run in-runner on a PGDATA so it bakes into the artifact,
# same idea as the mariadb datadir. Trust auth (throwaway CI) skips password setup; durability
# off for speed. Postgres is preinstalled on ubuntu-latest under /usr/lib/postgresql/<ver>/bin.
if [ "${DB:-mariadb}" = "postgres" ]; then
PG_BIN=$(ls -d /usr/lib/postgresql/*/bin 2>/dev/null | sort -V | tail -1)
[ -n "$PG_BIN" ] && export PATH="$PG_BIN:$PATH"
PGDATA="${CI_DB_DATADIR:-$HOME/frappe-bench/pgdata}"
if [ ! -d "$PGDATA/base" ]; then
initdb -D "$PGDATA" -U postgres --auth-local=trust --auth-host=trust >/dev/null
echo "host all all 127.0.0.1/32 trust" >> "$PGDATA/pg_hba.conf"
fi
pg_ctl -D "$PGDATA" -w -o "-p 5432 -c listen_addresses=127.0.0.1 -c unix_socket_directories=$PGDATA -c fsync=off -c synchronous_commit=off -c full_page_writes=off" start
echo "PostgreSQL up in-runner (pgdata=$PGDATA)"
exit 0
fi
# --- MariaDB ---
DATADIR="${CI_DB_DATADIR:-$HOME/frappe-bench/mariadb-data}"
SOCK="$DATADIR/mysqld.sock"
fresh=0
if [ ! -d "$DATADIR/mysql" ]; then
mkdir -p "$DATADIR"
mariadb-install-db --no-defaults --datadir="$DATADIR" \
--auth-root-authentication-method=normal --skip-test-db >/dev/null 2>&1
fresh=1
fi
# Throwaway-CI durability off; bind TCP 127.0.0.1:3306 so bench/install.sh connect as usual.
mariadbd --no-defaults --datadir="$DATADIR" --socket="$SOCK" --pid-file="$DATADIR/mysqld.pid" \
--port=3306 --bind-address=127.0.0.1 \
--innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --skip-log-bin \
> "$HOME/mariadb.log" 2>&1 &
up=0
for _ in $(seq 1 60); do
if mariadb-admin --socket="$SOCK" ping --silent 2>/dev/null; then up=1; break; fi
sleep 1
done
# Fail loudly instead of letting the loop fall through (exit 0 of the last `sleep`) into SQL that
# would error with a vague socket-connection failure.
[ "$up" = "1" ] || { echo "mariadbd did not come up on $SOCK"; cat "$HOME/mariadb.log" 2>/dev/null; exit 1; }
if [ "$fresh" = "1" ]; then
# A fresh datadir has only a password-less root@localhost. Give it the password install.sh
# uses, plus a TCP-reachable root@127.0.0.1, so the rest of install.sh works unchanged.
mariadb --no-defaults --socket="$SOCK" -u root <<'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;
SQL
fi
echo "MariaDB up in-container (datadir=$DATADIR, fresh=$fresh)"

View File

@@ -21,7 +21,7 @@ jobs:
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@v3.0.0
semgrep:
name: semgrep

View File

@@ -22,4 +22,4 @@ jobs:
pull-requests: write
steps:
- uses: alyf-de/po-review-action@v1.1.0
- uses: alyf-de/po-review-action@v1.0.0

View File

@@ -31,49 +31,51 @@ on:
permissions:
contents: read
packages: read
concurrency:
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
# Shared across both jobs. Both run in the SAME CI image so the bench lives at the identical
# path (/home/ci/frappe-bench) on the setup runner and the test shards — that's what makes the
# packaged Python venv portable between them.
env:
TZ: 'Asia/Kolkata'
DEBIAN_FRONTEND: noninteractive
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
ERPNEXT_CI_USER: ci
PIP_CACHE_DIR: /home/ci/.cache/pip
npm_config_cache: /home/ci/.cache/npm
YARN_CACHE_FOLDER: /home/ci/.cache/yarn
UV_CACHE_DIR: /home/ci/.cache/uv
jobs:
# Build the bench (clone + pip + yarn + assets) and reinstall test_site ONCE, on a free
# GitHub-hosted runner, then publish the whole bench (with a DB dump baked in) as an artifact.
# The expensive, non-parallelisable work happens here exactly once instead of on every shard.
setup:
name: Build & reinstall (setup)
# Dedicated scale set (fat cpu request) so the build+reinstall runs at full speed, uncontended
# by the many thin test shards. Same CI image + /home/ci path + 127.0.0.1 DB as the shards,
# so the packaged bench (and its venv) transplants cleanly.
runs-on: erpnext-arc-setup
timeout-minutes: 40
container:
image: ghcr.io/frappe/erpnext-ci-mariadb:py3.14-node24
credentials:
username: ${{ secrets.GHCR_USERNAME || github.actor }}
password: ${{ secrets.GHCR_TOKEN || github.token }}
defaults:
run:
shell: bash
test:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
name: Python Unit Tests
services:
mysql:
image: mariadb:10.6
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
# Disable durability guarantees that are unnecessary in a throwaway CI container.
# innodb_flush_log_at_trx_commit=0 avoids an fsync on every commit (biggest win).
# sync_binlog=0 skips binary-log syncs; innodb_doublewrite=0 skips the doublewrite buffer.
MARIADB_EXTRA_FLAGS: --innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --innodb-doublewrite=0
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -fq "${GITHUB_WORKSPACE}"
@@ -82,17 +84,53 @@ jobs:
exit 1
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# MariaDB runs in-container on a datadir OUTSIDE the bench, because install.sh's next step
# does `rm -rf ~/frappe-bench`. After the reinstall, the datadir is moved into the bench so
# it ships in the artifact — test shards then start an already-loaded server (no restore).
- name: Start DB
run: bash ${GITHUB_WORKSPACE}/.github/helper/start-db.sh
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
env:
SKIP_SYSTEM_SETUP: "1"
CI_DB_DATADIR: /home/ci/db-data
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache wkhtmltopdf
uses: actions/cache@v4
with:
path: /tmp/wkhtmltox.deb
key: wkhtmltox-0.12.6.1-2-jammy-amd64
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
@@ -101,81 +139,9 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
DB_HOST: 127.0.0.1
DB_USER_HOST: '%'
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb
SKIP_SYSTEM_SETUP: "1"
SKIP_WKHTMLTOX_SETUP: "1"
# Clean shutdown (consistent InnoDB datadir), then stage it inside the bench for packaging.
- name: Stop DB and stage datadir
run: |
mariadb-admin -h 127.0.0.1 -P 3306 -u root -proot shutdown || true
for _ in $(seq 1 30); do [ -f /home/ci/db-data/mysqld.pid ] || break; sleep 1; done
# Don't bake a dirty datadir — fail if mariadbd didn't finish stopping, rather than ship
# an inconsistent datadir the shards would have to crash-recover.
[ -f /home/ci/db-data/mysqld.pid ] && { echo "mariadbd did not shut down cleanly"; exit 1; }
mv /home/ci/db-data /home/ci/frappe-bench/mariadb-data
# Package the whole bench (apps, venv, node_modules, sites, the DB dump, and hydrate.sh)
# into one artifact for the test shards to consume.
# Single-node hand-off: stage the bench on a node-local hostPath instead of round-tripping
# through GitHub artifact storage (~60s/shard). Setup and shards share the same disk, so
# the shards just untar it locally. NOTE: this assumes one node — a shard on a different
# node could not read this path (then you'd need GitHub artifacts or an NFS/RWX volume).
- name: Stage bench on node (hostPath)
run: |
cp "${GITHUB_WORKSPACE}/.github/helper/hydrate.sh" /home/ci/frappe-bench/hydrate.sh
cp "${GITHUB_WORKSPACE}/.github/helper/start-db.sh" /home/ci/frappe-bench/start-db.sh
mkdir -p /opt/ci-bench-staging
# self-clean: drop bench tars from runs older than 2h
find /opt/ci-bench-staging -maxdepth 1 -name '*.tar.gz' -mmin +120 -delete 2>/dev/null || true
# Exclude .git/node_modules; the mariadb-data datadir IS included (the pre-loaded DB).
tar czpf "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz" -C /home/ci \
--exclude='.git' --exclude='node_modules' frappe-bench
ls -lh "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz"
# Fan-out: each shard downloads the bench, untars it, starts MariaDB on the baked datadir, and
# runs its slice of the suite. No clone, no build, no reinstall, no DB dump restore on the shards.
test:
name: Python Unit Tests
needs: setup
runs-on: erpnext-arc
timeout-minutes: 60
container:
image: ghcr.io/frappe/erpnext-ci-mariadb:py3.14-node24
credentials:
username: ${{ secrets.GHCR_USERNAME || github.actor }}
password: ${{ secrets.GHCR_TOKEN || github.token }}
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
steps:
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# Read the bench straight from the node-local hostPath the setup job staged it on — no
# GitHub download. -p preserves the ci (uid 1001) ownership so bench runs as ci cleanly.
- name: Untar bench from node (hostPath)
run: |
tar xzpf "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz" -C /home/ci
ls -ld /home/ci/frappe-bench
- name: Hydrate (start DB on baked datadir + bench start)
run: bash /home/ci/frappe-bench/hydrate.sh
env:
DB_HOST: 127.0.0.1
SKIP_SYSTEM_SETUP: "1"
- name: Run Tests
run: |
su -m "${ERPNEXT_CI_USER:-frappe}" -s /bin/bash <<'EOF'
cd ~/frappe-bench/
coverage_flag=""
if [ "$WITH_COVERAGE" = "true" ]; then coverage_flag="--with-coverage"; fi
@@ -183,10 +149,10 @@ jobs:
--total-builds ${{ strategy.job-total }} \
--build-number ${{ matrix.container }} \
$coverage_flag
EOF
env:
TYPE: server
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true
@@ -196,11 +162,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
path: /home/ci/frappe-bench/sites/coverage.xml
path: /home/runner/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: [test]
needs: test
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:

View File

@@ -48,6 +48,7 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.15.1"
__version__ = "16.25.0"
def get_default_company(user=None):

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class OverlapError(frappe.ValidationError):
@@ -37,20 +36,8 @@ class AccountingPeriod(Document):
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_overlap()
def validate_dates(self):
if getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("Start Date cannot be after End Date"))
if getdate(self.end_date) > getdate(nowdate()):
frappe.throw(
_(
"Accounting Period cannot be created for a future date. End Date {0} is after today."
).format(frappe.bold(frappe.format(self.end_date, "Date")))
)
def before_insert(self):
self.bootstrap_doctypes_for_closing()

View File

@@ -3,7 +3,7 @@
import unittest
import frappe
from frappe.utils import nowdate
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
@@ -94,7 +94,7 @@ def create_accounting_period(**args):
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})

View File

@@ -10,22 +10,75 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentLedgerEntry(ERPNextTestSuite):
def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry")
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.creditors = "Creditors - _TC"
self.bank = "Cash - _TC"
self.item = "_Test Item"
self.customer = "_Test Customer"
self.create_company()
self.create_item()
self.create_customer()
def create_company(self):
company_name = "_Test Payment Ledger"
company = None
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PL"
self.income_account = "Sales - _PL"
self.expense_account = "Cost of Goods Sold - _PL"
self.debit_to = "Debtors - _PL"
self.creditors = "Creditors - _PL"
# create bank account
if frappe.db.exists("Account", "HDFC - _PL"):
self.bank = "HDFC - _PL"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PL",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item_name = "_Test PL Item"
item = create_item(
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test PL Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -98,6 +151,18 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
)
return so
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()

View File

@@ -19,6 +19,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
@@ -27,10 +28,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company="Test PCV Company",
company=company,
save=False,
)
jv1.company = "Test PCV Company"
jv1.company = company
jv1.save()
jv1.submit()
@@ -40,10 +41,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cost of Goods Sold - TPC",
account2="Cash - TPC",
cost_center=cost_center,
company="Test PCV Company",
company=company,
save=False,
)
jv2.company = "Test PCV Company"
jv2.company = company
jv2.save()
jv2.submit()
@@ -67,13 +68,14 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Main")
cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company="Test PCV Company",
company=company,
cost_center=cost_center1,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -84,7 +86,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
posting_date="2021-03-15",
)
create_sales_invoice(
company="Test PCV Company",
company=company,
cost_center=cost_center2,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -129,11 +131,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
)
def test_period_closing_with_finance_book_entries(self):
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
company="Test PCV Company",
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
@@ -150,9 +153,9 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
amount=400,
cost_center=cost_center,
posting_date="2021-03-15",
company="Test PCV Company",
company=company,
)
jv.company = "Test PCV Company"
jv.company = company
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
@@ -179,6 +182,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertSequenceEqual(pcv_gle, expected_gle)
def test_gl_entries_restrictions(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
@@ -189,15 +193,16 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company="Test PCV Company",
company=company,
save=False,
)
jv1.company = "Test PCV Company"
jv1.company = company
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
@@ -207,10 +212,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
company="Test PCV Company",
company=company,
save=False,
)
jv1.company = "Test PCV Company"
jv1.company = company
jv1.save()
jv1.submit()
@@ -220,10 +225,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company="Test PCV Company",
company=company,
save=False,
)
jv2.company = "Test PCV Company"
jv2.company = company
jv2.save()
jv2.submit()
@@ -250,11 +255,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company="Test PCV Company",
company=company,
save=False,
)
jv3.company = "Test PCV Company"
jv3.company = company
jv3.save()
jv3.submit()
@@ -289,12 +294,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
warehouse = frappe.db.get_value("Warehouse", {"company": "Test PCV Company"}, "name")
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": "Test PCV Company",
"company": company,
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
@@ -335,6 +340,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv = make_journal_entry(
@@ -343,10 +349,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company="Test PCV Company",
company=company,
save=False,
)
jv.company = "Test PCV Company"
jv.company = company
jv.save()
jv.submit()
@@ -372,6 +378,19 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
def create_company():
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "Test PCV Company",
"country": "United States",
"default_currency": "USD",
}
)
company.insert(ignore_if_duplicate=True)
return company.name
def create_account():
account = frappe.get_doc(
{

View File

@@ -416,6 +416,7 @@ def get_context(customer, doc):
return {
"doc": template_doc,
"customer": frappe.get_doc("Customer", customer),
"frappe": frappe.utils,
}

View File

@@ -2928,24 +2928,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_po_is_blocked(self):
service_item = create_item(
"_Test Service Item Non Stock PI",
is_stock_item=0,
is_purchase_item=1,
).name
po = create_purchase_order(item_code=service_item, qty=5, rate=100, do_not_save=False)
po.submit()
pi = make_pi_from_po(po.name)
pi.items[0].qty = 10 # overbill by 100 %
pi.save()
with self.assertRaises(frappe.ValidationError):
pi.submit()
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
pi = make_purchase_invoice(do_not_save=True)
discount_amount = 7

View File

@@ -2150,14 +2150,11 @@ class TestSalesInvoice(ERPNextTestSuite):
def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = "Percentage"
si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
# set rate to zero, so that it is recalculated on save
si.items[0].rate = 0
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
@@ -3868,51 +3865,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertTrue("cannot overbill" in str(err.exception).lower())
dn.cancel()
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_is_blocked(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
service_item = create_item(
"_Test Service Item Non Stock SI",
is_stock_item=0,
).name
so = make_sales_order(item_code=service_item, qty=5, rate=100)
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_from_quotation_is_blocked(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order as make_so_from_quotation
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
service_item = create_item(
"_Test Service Item Non Stock SI Quot",
is_stock_item=0,
).name
quotation = make_quotation(item_code=service_item, qty=5, rate=100)
so = make_so_from_quotation(quotation.name)
so.delivery_date = frappe.utils.add_days(frappe.utils.today(), 7)
so.insert()
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{

View File

@@ -1,26 +0,0 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:middle ! important\">\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\t\tcompany_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\">\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\", \"city\",\n\t\t\t\t\"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address %} {{\n\t\t\t\tcompany_address.address_line1 or \"\" }}<br>\n\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\">\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %} {% set email =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"email\") %} {% set phone_no =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ doc.doctype }}</span>\n\t\t\t\t\t<span>{{ doc.name }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 15:21:48.255627",
"custom_css": "\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tpadding-right: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\n\t.letter-head td {\n\t\tpadding: 0px !important;\n\t}\n\t.invoice-header {\n\t\twidth: 100%;\n\t}\n\t.logo-cell {\n\t\twidth: 100px;\n\t\ttext-align: center;\n\t\tposition: relative;\n\t}\n\t.logo-container {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t}\n\t.logo-container img {\n\t\tmax-width: 90px;\n\t\tmax-height: 90px;\n\t\tdisplay: inline-block;\n\t\tborder-radius: 15px;\n\t}\n\t.company-details {\n\t\twidth: 40%;\n\t\talign-content: center;\n\t}\n\t.company-name {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: #171717;\n\t\tmargin-bottom: 4px;\n\t}\n\t.invoice-info-cell {\n\t\tfloat: right;\n\t\tvertical-align: top;\n\t}\n\t.invoice-info {\n\t\tmargin-bottom: 2px;\n\t}\n\t.invoice-label {\n\t\tcolor: #7c7c7c;\n\t\tdisplay: inline-block;\n\t\tmargin-right: 5px;\n\t}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead",
"modified": "2026-06-24 17:49:52.350750",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -1,26 +0,0 @@
{
"align": "Left",
"content": "<table class=\"letterhead-container\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-address\">\n\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\tcompany_logo %}\n\t\t\t\t<div class=\"logo\">\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\">\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\",\n\t\t\t\t\t\"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address\n\t\t\t\t\t%} {{ company_address.address_line1 or \"\" }}<br>\n\t\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td style=\"vertical-align:top\">\n\t\t\t\t<div style=\"height:90px;margin-bottom:10px;text-align:right\">\n\t\t\t\t\t<div class=\"invoice-title\">{{ doc.doctype }}</div>\n\t\t\t\t\t<div class=\"invoice-number\">{{ doc.name }}</div>\n\t\t\t\t\t<br>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"text-align:left;float:right\" class=\"other-details\">\n\t\t\t\t\t{% if doc.company %}{% set company_details = frappe.db.get_value(\"Company\", doc.company, [\"website\", \"email\",\n\t\t\t\t\t\"phone_no\"], as_dict=True) %}{% set website = company_details.website %}{% set email =\n\t\t\t\t\tcompany_details.email %}{% set phone_no = company_details.phone_no %}{% else %}{% set website = None %}{% set email = None %}{% set phone_no = None %}{% endif %} {% if website %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Website:\") }}</span><span class=\"contact-value\">{{ website }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Email:\") }}</span><span class=\"contact-value\">{{ email }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Contact:\") }}</span><span class=\"contact-value\">{{ phone_no }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n",
"creation": "2026-05-15 15:21:48.373815",
"custom_css": "\t.print-format-preview {\n\t\tmargin-top: 12px;\n\t}\n\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tbackground: #f8f8f8;\n\t\tpadding: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\t.letterhead-container {\n\t\twidth: 100%;\n\t}\n\t.letterhead-container .other-details {\n\t\tposition: absolute;\n\t\tright: 0;\n\t\tbottom: 0;\n\t}\n\t.logo-address {\n\t\twidth: 65%;\n\t\tvertical-align: top;\n\t}\n\n\t.letter-head .logo {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t\tmargin-bottom: 10px;\n\t}\n\n\t.letter-head .logo img {\n\t\tborder-radius: 15px;\n\t}\n\n\t.company-name {\n\t\tcolor: #171717;\n\t\tfont-weight: bold;\n\t\tline-height: 23px;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.company-address {\n\t\tcolor: #171717;\n\t\twidth: 300px;\n\t}\n\n\t.invoice-title {\n\t\tfont-weight: bold;\n\t}\n\n\t.invoice-number {\n\t\tcolor: #7c7c7c;\n\t}\n\n\t.contact-title {\n\t\tcolor: #7c7c7c;\n\t\twidth: 60px;\n\t\tdisplay: inline-block;\n\t\tvertical-align: top;\n\t\tmargin-right: 10px;\n\t}\n\n\t.contact-value {\n\t\tcolor: #171717;\n\t\tdisplay: inline-block;\n\t}\n\t.letterhead-container td {\n\t\tpadding: 0px !important;\n\t\tposition: relative;\n\t}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead - Grey",
"modified": "2026-06-24 18:23:05.120521",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead - Grey",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -1,26 +0,0 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:top\">\n\t\t\t\t{% if doc.company %}{% set company = frappe.get_doc(\"Company\", doc.company) %}{% else %}{% set company = frappe._dict() %}{% endif %}\n\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% if company.company_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company.company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\" style=\"vertical-align:top\">\n\t\t\t\t{% if company.name %}<div class=\"company-name\">{{ company.name }}</div>{% endif %}\n\n\t\t\t\t{% set company_address_name = frappe.db.get_value(\n\t\t\t\t\t\"Dynamic Link\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"link_doctype\": \"Company\",\n\t\t\t\t\t\t\"link_name\": company.name,\n\t\t\t\t\t\t\"parenttype\": \"Address\"\n\t\t\t\t\t},\n\t\t\t\t\t\"parent\"\n\t\t\t\t) %}\n\n\t\t\t\t{% if company_address_name %}\n\t\t\t\t\t{% set company_address = frappe.db.get_value(\n\t\t\t\t\t\t\"Address\",\n\t\t\t\t\t\tcompany_address_name,\n\t\t\t\t\t\t[\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"],\n\t\t\t\t\t\tas_dict=True\n\t\t\t\t\t) %}\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if company_address %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{{ company_address.address_line1 or \"\" }}\n\n\t\t\t\t\t{% if company_address.address_line2 %}\n\t\t\t\t\t\t<br>{{ company_address.address_line2 }}\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t<br>\n\n\t\t\t\t\t{{ company_address.city or \"\" }}\n\t\t\t\t\t{% if company_address.state %}, {{ company_address.state }}{% endif %}\n\t\t\t\t\t{{ company_address.pincode or \"\" }}\n\n\t\t\t\t\t{% if company_address.country %}\n\t\t\t\t\t\t, {{ company_address.country }}\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\" style=\"vertical-align:top;text-align:right\">\n\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %}\n\t\t\t\t{% set email = frappe.db.get_value(\"Company\", doc.company, \"email\") %}\n\t\t\t\t{% set phone_no = frappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 19:49:47.582252",
"custom_css": ".letter-head {\n\tborder-radius: 18px;\n\tpadding: 8px 10px;\n\tmargin: 10px 0 14px;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tcolor: #171717;\n}\n\n.letter-head td {\n\tpadding: 0 !important;\n\tvertical-align: middle;\n}\n\n.invoice-header {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\ttable-layout: fixed;\n\tborder-bottom: 1px solid #ededed;\n\tpadding-bottom: 10px;\n}\n\n.logo-cell {\n\twidth: 100px;\n\ttext-align: center;\n\twhite-space: nowrap;\n}\n\n.logo-container {\n\tdisplay: inline-block;\n\tmargin: auto;\n}\n\n.logo-container img {\n\tmax-width: 95px;\n\tmax-height: 95px;\n\tdisplay: block;\n\tborder-radius: 12px;\n}\n\n.company-details {\n\twidth: 55%;\n\tpadding-left: 10px !important;\n\tline-height: 1.5;\n}\n\n.company-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 4px;\n}\n\n.company-address {\n\tfont-size: 14px;\n\tline-height: 1.5;\n\tcolor: #171717;\n}\n\n.invoice-info-cell {\n\twidth: 240px;\n\ttext-align: right;\n\tvertical-align: top !important;\n\tline-height: 1.5;\n}\n\n.document-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 6px;\n}\n\n.invoice-info {\n\tfont-size: 14px;\n\tcolor: #171717;\n\tmargin-bottom: 2px;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.invoice-label {\n\tcolor: #7c7c7c;\n\tfont-weight: 500;\n\tmargin-right: 4px;\n\tdisplay: inline-block;\n}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "Report",
"letter_head_name": "Company Letterhead Report",
"modified": "2026-06-24 18:06:39.820968",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead Report",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -927,15 +927,6 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
if party_type == "Supplier":
info["total_unpaid"] = -1 * info["total_unpaid"]
if info["total_unpaid"] < 0:
info["balance_label"] = (
"Total Advance Paid" if party_type == "Supplier" else "Total Advance Received"
)
info["balance_amount"] = abs(info["total_unpaid"])
else:
info["balance_label"] = "Total Unpaid"
info["balance_amount"] = info["total_unpaid"]
company_wise_info.append(info)
return company_wise_info

View File

@@ -76,7 +76,6 @@ def get_ratios_data(filters, period_list, years):
cogs, total_expense = {}, {}
quick_asset = {}
direct_expense = {}
fixed_asset = {}
for year in years:
total_quick_asset = 0
@@ -94,7 +93,6 @@ def get_ratios_data(filters, period_list, years):
quick_asset,
total_quick_asset,
],
[fixed_asset, total_asset, "Fixed Asset", year, assets, "Asset", {}, 0],
[
current_liability,
total_liability,
@@ -114,7 +112,7 @@ def get_ratios_data(filters, period_list, years):
add_solvency_ratios(
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
)
add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense)
add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense)
return data
@@ -195,7 +193,7 @@ def add_solvency_ratios(
data.append(return_on_equity_ratio)
def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense):
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": _("Turnover Ratios")})
@@ -210,7 +208,7 @@ def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sale
)
ratio_data = [
[_("Fixed Asset Turnover Ratio"), net_sales, fixed_asset],
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
[_("Inventory Turnover Ratio"), cogs, avg_stock],

View File

@@ -1,73 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.utils import today
from erpnext.accounts.report.financial_ratios.financial_ratios import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestFinancialRatios(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.abbr = "_TC"
# The report matches the group accounts by their account_type, which the
# standard chart of accounts does not set on group accounts by default.
self.set_account_type("Fixed Assets", "Fixed Asset")
self.set_account_type("Direct Income", "Direct Income")
def set_account_type(self, account_name, account_type):
frappe.db.set_value("Account", f"{account_name} - {self.abbr}", "account_type", account_type)
def test_fixed_asset_turnover_uses_net_fixed_assets(self):
# Acquire a fixed asset worth 10,000 funded by equity.
self.make_journal_entry("Buildings", "Capital Stock", 10000)
# Book sales of 20,000 collected in cash. Total assets now = 30,000
# (Buildings 10,000 + Cash 20,000), while net fixed assets stay at 10,000.
self.make_journal_entry("Cash", "Sales", 20000)
columns, data = execute(self.get_report_filters())
year_key = columns[1]["fieldname"]
ratio_row = next((row for row in data if row.get("ratio") == "Fixed Asset Turnover Ratio"), None)
self.assertIsNotNone(ratio_row, "Fixed Asset Turnover Ratio row not found in report output")
# Net Sales / Net Fixed Assets = 20,000 / 10,000 = 2.0
# (the old behaviour divided by total assets, giving 20,000 / 30,000 = 0.667)
self.assertEqual(ratio_row[year_key], 2.0)
def get_report_filters(self):
active_fy = frappe.db.get_value(
"Fiscal Year",
{"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
["name", "year_start_date", "year_end_date"],
as_dict=True,
)
return frappe._dict(
company=self.company,
from_fiscal_year=active_fy.name,
to_fiscal_year=active_fy.name,
period_start_date=active_fy.year_start_date,
period_end_date=active_fy.year_end_date,
filter_based_on="Fiscal Year",
periodicity="Yearly",
)
def make_journal_entry(self, debit_account, credit_account, amount):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = today()
journal_entry.company = self.company
for account, debit, credit in (
(debit_account, amount, 0),
(credit_account, 0, amount),
):
journal_entry.append(
"accounts",
{
"account": f"{account} - {self.abbr}",
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
},
)
journal_entry.insert()
journal_entry.submit()

View File

@@ -14,17 +14,71 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGrossProfit(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.finished_warehouse = "Finished Goods - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.item = "_Test Item"
self.item2 = "_Test Item Home Desktop 100"
self.bundle = "_Test Product Bundle Item"
self.customer = "_Test Customer"
self.create_company()
self.create_item()
self.create_bundle()
self.create_customer()
def create_company(self):
company_name = "_Test Gross Profit"
abbr = "_GP"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
item2 = create_item(
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item2 = item2 if isinstance(item2, str) else item2.item_code
# This will be parent item
bundle = create_item(
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
# Create Product Bundle
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -158,7 +212,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 150.0,
@@ -187,7 +241,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
@@ -219,7 +273,7 @@ class TestGrossProfit(ERPNextTestSuite):
"item_code": self.item2,
"s_warehouse": "",
"t_warehouse": self.finished_warehouse,
"qty": 2,
"qty": 1,
"basic_rate": 100,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
@@ -319,7 +373,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": 4.0,
"avg._selling_rate": 100.0,
"valuation_rate": 125.0,
@@ -360,10 +414,10 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
"avg._selling_rate": 100,
"valuation_rate": 0.0,
"selling_amount": 0.0,
"buying_amount": 0.0,
"gross_profit": 0.0,
@@ -383,7 +437,7 @@ class TestGrossProfit(ERPNextTestSuite):
"""
# Make Cr Note
sinv = self.create_sales_invoice(
qty=-1, rate=200, posting_date=nowdate(), do_not_save=True, do_not_submit=True
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
@@ -406,14 +460,14 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": -1.0,
"avg._selling_rate": 200.0,
"valuation_rate": 100.0,
"selling_amount": -200.0,
"buying_amount": -100.0,
"avg._selling_rate": 100.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": -50.0,
"gross_profit_%": -100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
@@ -499,7 +553,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _TC",
"warehouse": "Stores - _GP",
"qty": 4.0,
"avg._selling_rate": 800.0,
"valuation_rate": 700.0,
@@ -562,7 +616,7 @@ class TestGrossProfit(ERPNextTestSuite):
def test_gross_profit_groupby_invoices(self):
create_sales_invoice(
qty=1,
rate=200,
rate=100,
company=self.company,
customer=self.customer,
item_code=self.item,
@@ -584,10 +638,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 200.0)
self.assertEqual(total.buying_amount, 100.0)
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 50.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
@@ -596,7 +650,7 @@ class TestGrossProfit(ERPNextTestSuite):
return_inv_date = add_days(month_end_date, 1)
# create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=200, do_not_save=True, do_not_submit=True)
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = sales_inv_date
sinv.save().submit()
@@ -615,10 +669,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 200.0)
self.assertEqual(total.buying_amount, 100.0)
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 50.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
# extend filters upto returned period
filters.update({"to_date": return_inv_date})
@@ -636,10 +690,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, -200.0)
self.assertEqual(total.buying_amount, -100.0)
self.assertEqual(total.selling_amount, -100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, -100.0)
self.assertEqual(total.get("gross_profit_%"), -50.0)
self.assertEqual(total.get("gross_profit_%"), -100.0)
def test_sales_person_wise_gross_profit(self):
sales_person = make_sales_person("_Test Sales Person")
@@ -670,10 +724,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total[5], 1000.0) # selling amount
self.assertEqual(total[6], 1000.0) # buying amount
self.assertEqual(total[7], 0.0) # gross profit
self.assertEqual(total[8], 0.0) # gross profit %
self.assertEqual(total[5], 1000.0)
self.assertEqual(total[6], 0.0)
self.assertEqual(total[7], 1000.0)
self.assertEqual(total[8], 100.0)
def test_drop_ship(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice

View File

@@ -9,12 +9,42 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentLedger(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.create_company()
self.cleanup()
def cleanup(self):
doctypes = []
doctypes.append(qb.DocType("GL Entry"))
doctypes.append(qb.DocType("Payment Ledger Entry"))
doctypes.append(qb.DocType("Sales Invoice"))
doctypes.append(qb.DocType("Payment Entry"))
for doctype in doctypes:
qb.from_(doctype).delete().where(doctype.company == self.company).run()
def create_company(self):
name = "Test Payment Ledger"
company = None
if frappe.db.exists("Company", name):
company = frappe.get_doc("Company", name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses" + " - " + company.abbr
self.income_account = company.default_income_account
self.expense_account = company.default_expense_account
self.debit_to = company.default_receivable_account
def test_unpaid_invoice_outstanding(self):
sinv = create_sales_invoice(

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _, msgprint
from frappe.model.meta import get_field_precision
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate
from pypika.terms import Bracket, LiteralValue, Order
@@ -126,32 +125,17 @@ def _execute(filters=None, additional_table_columns=None):
row.update({frappe.scrub(tax_acc): tax_amount})
# total tax, grand total, rounded total & outstanding amount
outstanding_precision = (
get_field_precision(
frappe.get_meta("Purchase Invoice").get_field("outstanding_amount"),
currency=company_currency,
)
or 2
)
row.update(
{
"total_tax": total_tax,
"grand_total": inv.base_grand_total,
"rounded_total": inv.base_rounded_total,
"outstanding_amount": inv.outstanding_amount,
}
)
if inv.doctype == "Purchase Invoice":
row.update(
{
"debit": inv.base_grand_total,
"credit": 0.0,
"outstanding_amount": flt(
(inv.outstanding_amount * (inv.conversion_rate or 1)), outstanding_precision
),
}
)
row.update({"debit": inv.base_grand_total, "credit": 0.0})
else:
row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
@@ -411,7 +395,6 @@ def get_invoices(filters, additional_query_columns):
pi.base_rounded_total,
pi.outstanding_amount,
pi.mode_of_payment,
pi.conversion_rate,
)
.where(pi.docstatus == 1)
)

View File

@@ -2,7 +2,7 @@
# MIT License. See license.txt
import frappe
from frappe.utils import add_months, flt, today
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -67,35 +67,6 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_currency_conversion(self):
usd_creditors = frappe.get_doc(
{
"doctype": "Account",
"account_name": "USD Creditors",
"parent_account": "Accounts Payable - _TC",
"company": "_Test Company",
"account_type": "Payable",
"root_type": "Liability",
"report_type": "Balance Sheet",
"account_currency": "USD",
}
).insert()
foreign_invoice = make_purchase_invoice()
foreign_invoice.db_set("currency", "USD")
foreign_invoice.db_set("conversion_rate", 80)
foreign_invoice.db_set("credit_to", usd_creditors.name)
foreign_invoice.db_set("outstanding_amount", 100.236)
local_invoice = make_purchase_invoice()
local_invoice.db_set("currency", "INR")
local_invoice.db_set("conversion_rate", 1)
local_invoice.db_set("outstanding_amount", 200.456)
columns, data, *_ = execute(frappe._dict({"company": foreign_invoice.company}))
outstanding_precision = 2
data_by_name = {x.get("voucher_no"): x.get("outstanding_amount") for x in data}
self.assertEqual(data_by_name.get(foreign_invoice.name), flt((100.236 * 80), outstanding_precision))
self.assertEqual(data_by_name.get(local_invoice.name), flt(200.456, outstanding_precision))
def test_purchase_register_ledger_view(self):
filters = frappe._dict(
company="_Test Company 6",

View File

@@ -141,31 +141,17 @@ def _execute(filters, additional_table_columns=None):
# total tax, grand total, outstanding amount & rounded total
outstanding_precision = (
get_field_precision(
frappe.get_meta("Sales Invoice").get_field("outstanding_amount"),
currency=company_currency,
)
or 2
)
row.update(
{
"tax_total": total_tax,
"grand_total": inv.base_grand_total,
"rounded_total": inv.base_rounded_total,
"outstanding_amount": inv.outstanding_amount,
}
)
if inv.doctype == "Sales Invoice":
row.update(
{
"debit": inv.base_grand_total,
"credit": 0.0,
"outstanding_amount": flt(
(inv.outstanding_amount * (inv.conversion_rate or 1)), outstanding_precision
),
}
)
row.update({"debit": inv.base_grand_total, "credit": 0.0})
else:
row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
@@ -451,7 +437,6 @@ def get_invoices(filters, additional_query_columns):
si.is_internal_customer,
si.represents_company,
si.company,
si.conversion_rate,
)
.where(si.docstatus == 1)
)

View File

@@ -1,10 +1,9 @@
import frappe
from frappe.utils import add_days, flt, getdate, today
from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.customer.test_customer import make_customer
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
@@ -217,25 +216,3 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
}
result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result}
self.assertDictEqual(result_output, expected_result)
def test_outstanding_currency_conversion(self):
foreign_invoice = create_sales_invoice(
customer="_Test Customer",
posting_date=add_days(today(), -1),
qty=1,
rate=100,
)
foreign_invoice.db_set("currency", "USD")
foreign_invoice.db_set("conversion_rate", 80)
foreign_invoice.db_set("outstanding_amount", 100.236)
make_customer("_Test Customer2")
local_invoice = create_sales_invoice(
customer="_Test Customer2", currency="INR", conversion_rate=1, qty=1, rate=200
)
local_invoice.db_set("outstanding_amount", 200.456)
columns, data, *_ = execute(frappe._dict({"company": foreign_invoice.company}))
outstanding_precision = 2
data_by_name = {x.get("voucher_no"): x.get("outstanding_amount") for x in data}
self.assertEqual(data_by_name.get(foreign_invoice.name), flt((100.236 * 80), outstanding_precision))
self.assertEqual(data_by_name.get(local_invoice.name), flt(200.456, outstanding_precision))

View File

@@ -100,9 +100,6 @@ class AssetCapitalization(StockController):
self.set_asset_values()
self.calculate_totals()
self.set_title()
# Asset Capitalization overrides validate() without calling super(), so the shared
# mandatory inventory dimension check must be invoked explicitly here.
self.validate_inventory_dimension_mandatory()
def on_update(self):
if self.stock_items:

View File

@@ -110,7 +110,6 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
},
{
@@ -562,7 +561,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-27 16:12:33.190257",
"modified": "2026-06-22 12:23:09.241125",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -4153,7 +4153,6 @@ def update_child_qty_rate(
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
child_item.discount_amount = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
@@ -4161,11 +4160,14 @@ def update_child_qty_rate(
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = child_item.price_list_rate
child_item.discount_percentage = 0
child_item.discount_amount = flt(child_item.rate_with_margin) - flt(child_item.rate)
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:

View File

@@ -383,17 +383,15 @@ class StatusUpdater(Document):
def fetch_items_with_pending_qty(self, args, item_field, items):
doctype = frappe.qb.DocType(args["target_dt"])
item_field_col = doctype[item_field]
item_field = doctype[item_field]
target_ref_field = doctype[args["target_ref_field"]]
target_field = doctype[args["target_field"]]
is_qty_check = "qty" in args["target_ref_field"]
query = (
return (
frappe.qb.from_(doctype)
.select(
doctype.name,
item_field_col.as_("item_code"),
item_field.as_("item_code"),
target_ref_field,
target_field,
doctype.parenttype,
@@ -402,18 +400,9 @@ class StatusUpdater(Document):
.where(target_ref_field < target_field)
.where(doctype.name.isin(items))
.where(doctype.docstatus == 1)
.run(as_dict=True)
)
if is_qty_check:
item_table = frappe.qb.DocType("Item")
query = (
query.join(item_table)
.on(item_table.name == item_field_col)
.where(item_table.is_stock_item == 1)
)
return query.run(as_dict=True)
def check_overflow_with_allowance(self, item, args):
"""
Checks if there is overflow considering a relaxation allowance.

View File

@@ -36,8 +36,6 @@ 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,
get_mandatory_dimension_fields,
get_mandatory_inventory_dimensions,
)
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -66,7 +64,6 @@ class StockController(AccountsController):
self.validate_internal_transfer()
self.validate_putaway_capacity()
self.reset_conversion_factor()
self.validate_inventory_dimension_mandatory()
def on_update(self):
super().on_update()
@@ -1143,50 +1140,6 @@ class StockController(AccountsController):
return item_account_wise_cost
def validate_inventory_dimension_mandatory(self):
# Mandatory inventory dimensions are enforced here (instead of via field-level `reqd`)
# so we can skip service rows and never block a document that is being cancelled.
if self.docstatus >= 2:
return
for table_field in ["items", "packed_items", "supplied_items"]:
rows = self.get(table_field)
if rows:
self.validate_mandatory_dimensions_in_table(rows)
def validate_mandatory_dimensions_in_table(self, rows):
child_doctype = rows[0].doctype
dimensions = get_mandatory_inventory_dimensions(child_doctype)
if not dimensions:
return
child_meta = frappe.get_meta(child_doctype)
for dimension in dimensions:
mandatory_fields = get_mandatory_dimension_fields(child_doctype, dimension)
for row in rows:
if mandatory_fields and not self.is_service_item_row(row):
self.validate_mandatory_dimension_row(row, dimension, mandatory_fields, child_meta)
def is_service_item_row(self, row) -> bool:
item_code = row.get("item_code")
return bool(item_code) and not frappe.get_cached_value("Item", item_code, "is_stock_item")
def validate_mandatory_dimension_row(self, row, dimension, mandatory_fields, child_meta):
for fieldname, condition in mandatory_fields:
if not child_meta.has_field(fieldname) or row.get(fieldname):
continue
if condition and not frappe.safe_eval(condition, {"doc": row, "parent": self}):
continue
frappe.throw(
_("Row #{0}: {1} is mandatory for the Inventory Dimension {2}.").format(
row.idx,
bold(_(child_meta.get_label(fieldname))),
bold(dimension.name),
)
)
def update_inventory_dimensions(self, row, sl_dict) -> None:
# To handle delivery note and sales invoice
if row.get("item_row"):

View File

@@ -165,85 +165,83 @@ class calculate_taxes_and_totals:
self.doc.conversion_rate = flt(self.doc.conversion_rate)
def calculate_item_rate(self, item):
if not item.price_list_rate:
remove_margin(item)
remove_discount(item)
item.rate_with_margin = 0
return
has_pricing_rules = item.pricing_rules and not self.doc.ignore_pricing_rule
if has_pricing_rules:
remove_margin(item)
for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc("Pricing Rule", d)
if not (
pricing_rule.margin_type
and pricing_rule.margin_rate_or_amount
and (
pricing_rule.margin_type == "Percentage" or pricing_rule.currency == self.doc.currency
)
):
continue
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
item.rate_with_margin = get_rate_with_margin(item)
if item.discount_percentage > 0:
item.discount_amount = flt(
item.rate_with_margin * item.discount_percentage / 100.0, item.precision("discount_amount")
)
calculated_rate = flt(item.rate_with_margin - item.discount_amount, item.precision("rate"))
# if rate is 0 or pricing rules are applicable, calculated rate is preferred
if has_pricing_rules or not item.rate:
item.rate = calculated_rate
return
# discount and margin are correct, exit early
if item.rate == calculated_rate:
return
# item rate does not match calculated rate. prefer item rate, reset margin / discount
if item.rate > item.price_list_rate:
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(
item.rate - item.price_list_rate, item.precision("margin_rate_or_amount")
)
item.rate_with_margin = item.rate
remove_discount(item)
return
item.rate_with_margin = item.price_list_rate
item.discount_amount = flt(item.rate_with_margin - item.rate, item.precision("discount_amount"))
item.discount_percentage = 0
remove_margin(item)
def calculate_item_values(self):
if self.doc.get("is_consolidated") or self.discount_amount_applied:
if self.doc.get("is_consolidated"):
return
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
for item in self.doc.items:
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
self.calculate_item_rate(item)
if not self.discount_amount_applied:
do_not_round_fields = ["valuation_rate", "incoming_rate"]
item.net_rate = item.rate
if not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt":
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
item.net_amount = item.amount
self._set_in_company_currency(
item, ["price_list_rate", "rate_with_margin", "rate", "net_rate", "amount", "net_amount"]
)
item.item_tax_amount = 0.0
for item in self.doc.items:
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
item.rate = flt(
item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)),
item.precision("rate"),
)
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in [
"Quotation Item",
"Sales Order Item",
"Delivery Note Item",
"Sales Invoice Item",
"POS Invoice Item",
"Purchase Invoice Item",
"Purchase Order Item",
"Purchase Receipt Item",
]:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(
item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)),
item.precision("rate"),
)
if item.discount_amount and not item.discount_percentage:
item.rate = item.rate_with_margin - item.discount_amount
else:
item.discount_amount = flt(
item.rate_with_margin - item.rate, item.precision("discount_amount")
)
elif flt(item.price_list_rate) > 0:
item.discount_amount = flt(
item.price_list_rate - item.rate, item.precision("discount_amount")
)
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
item.discount_amount = flt(
item.price_list_rate - item.rate, item.precision("discount_amount")
)
item.net_rate = item.rate
if (
not item.qty
and self.doc.get("is_return")
and self.doc.get("doctype") != "Purchase Receipt"
):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
item.net_amount = item.amount
self._set_in_company_currency(
item, ["price_list_rate", "rate", "net_rate", "amount", "net_amount"]
)
item.item_tax_amount = 0.0
def _set_in_company_currency(self, doc, fields):
"""set values in base currency"""
@@ -1137,6 +1135,48 @@ class calculate_taxes_and_totals:
self.calculate_outstanding_amount()
def calculate_margin(self, item):
rate_with_margin = 0.0
base_rate_with_margin = 0.0
if item.price_list_rate:
if item.pricing_rules and not self.doc.ignore_pricing_rule:
has_margin = False
for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc("Pricing Rule", d)
if pricing_rule.margin_rate_or_amount and (
(
pricing_rule.currency == self.doc.currency
and pricing_rule.margin_type in ["Amount", "Percentage"]
)
or pricing_rule.margin_type == "Percentage"
):
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
has_margin = True
if not has_margin:
item.margin_type = None
item.margin_rate_or_amount = 0.0
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(
item.rate - item.price_list_rate, item.precision("margin_rate_or_amount")
)
item.rate_with_margin = item.rate
elif item.margin_type and item.margin_rate_or_amount:
margin_value = (
item.margin_rate_or_amount
if item.margin_type == "Amount"
else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
)
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
return rate_with_margin, base_rate_with_margin
def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
@@ -1171,29 +1211,6 @@ class calculate_taxes_and_totals:
)
def remove_discount(item):
item.discount_percentage = 0.0
item.discount_amount = 0.0
def remove_margin(item):
item.margin_type = None
item.margin_rate_or_amount = 0.0
def get_rate_with_margin(item):
if not item.margin_type:
return item.price_list_rate
if item.margin_type == "Percentage":
return flt(
item.price_list_rate * (1 + (item.margin_rate_or_amount / 100.0)),
item.precision("rate_with_margin"),
)
return flt(item.price_list_rate + item.margin_rate_or_amount, item.precision("rate_with_margin"))
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return

View File

@@ -18,9 +18,39 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
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
from erpnext.tests.utils import ERPNextTestSuite
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.customer_type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.supplier_type = "Individual"
supplier.supplier_group = "All Supplier Groups"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name
class TestAccountsController(ERPNextTestSuite):
"""
Test Exchange Gain/Loss booking on various scenarios.
@@ -37,28 +67,79 @@ class TestAccountsController(ERPNextTestSuite):
"""
def setUp(self):
self.company = "_Test Company"
self.company_abbr = "_TC"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.finished_warehouse = "Finished Goods - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.debit_usd = "_Test Receivable USD - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.cash = "Cash - _TC"
self.creditors = "Creditors - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
self.item = "_Test Item"
self.customer = "_Test Customer USD"
self.supplier = "_Test Supplier USD"
self.create_company()
self.create_account()
self.create_item()
self.create_parties()
self.clear_old_entries()
frappe.flags.is_reverse_depr_entry = False
def create_company(self):
company_name = "_Test Company"
self.company_abbr = abbr = "_TC"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_parties(self):
self.create_customer()
self.create_supplier()
def create_customer(self):
self.customer = make_customer("_Test MC Customer USD", "USD")
def create_supplier(self):
self.supplier = make_supplier("_Test MC Supplier USD", "USD")
def create_account(self):
# Advance accounts are not in persistent test data — create them on demand.
accounts = [
frappe._dict(
{
"attribute_name": "debtors_usd",
"name": "Debtors USD",
"account_type": "Receivable",
"account_currency": "USD",
"parent_account": "Accounts Receivable - " + self.company_abbr,
}
),
frappe._dict(
{
"attribute_name": "creditors_usd",
"name": "Creditors USD",
"account_type": "Payable",
"account_currency": "USD",
"parent_account": "Accounts Payable - " + self.company_abbr,
}
),
# Advance accounts under Asset and Liability header
frappe._dict(
{
"attribute_name": "advance_received_usd",
@@ -104,7 +185,6 @@ class TestAccountsController(ERPNextTestSuite):
company.save()
customer = frappe.get_doc("Customer", self.customer)
customer.accounts = []
customer.append(
"accounts",
{
@@ -116,7 +196,6 @@ class TestAccountsController(ERPNextTestSuite):
customer.save()
supplier = frappe.get_doc("Supplier", self.supplier)
supplier.accounts = []
supplier.append(
"accounts",
{
@@ -242,6 +321,18 @@ class TestAccountsController(ERPNextTestSuite):
pinv.submit()
return pinv
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
@@ -864,7 +955,7 @@ class TestAccountsController(ERPNextTestSuite):
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = "_Test Customer"
sinv.customer = self.customer
sinv.company = self.company
sinv.currency = "INR"
sinv.taxes_and_charges = "_Test Tax - _TC"
@@ -880,7 +971,7 @@ class TestAccountsController(ERPNextTestSuite):
def test_19_fetch_taxes_based_on_item_tax_template_template(self):
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = "_Test Customer"
sinv.customer = self.customer
sinv.company = self.company
sinv.currency = "INR"
sinv.append(

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
from frappe.model.document import Document

View File

@@ -448,7 +448,6 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
out = frappe._dict()
lead_doc = frappe.get_doc("Lead", lead)
lead_doc.check_permission()
lead = lead_doc
out.update(

View File

@@ -136,7 +136,7 @@ def make_opportunity(source_name, target_doc=None):
@frappe.whitelist()
def get_opportunities(prospect):
return frappe.get_list(
return frappe.get_all(
"Opportunity",
filters={"opportunity_from": "Prospect", "party_name": prospect},
fields=[

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

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

@@ -694,11 +694,10 @@ frappe.ui.form.on("Job Card", {
// ── Wire up button click handlers ─────────────────────────────────
if (show_start) {
wrapper.find(".jcd-btn-start").on("click", () => {
const from_time = frappe.datetime.now_datetime();
const has_no_employee = !frm.doc.employee || !frm.doc.employee.length;
if (has_no_employee) {
// Capture the start time only when the employee dialog is submitted, not on click,
// so the time spent selecting the operator is not counted as worked time.
frappe.prompt(
{
fieldtype: "Table MultiSelect",
@@ -708,11 +707,11 @@ frappe.ui.form.on("Job Card", {
reqd: 1,
filters: { status: "Active" },
},
(d) => frm.events.start_timer(frm, frappe.datetime.now_datetime(), d.employees),
(d) => frm.events.start_timer(frm, from_time, d.employees),
__("Assign Job to Employee")
);
} else {
frm.events.start_timer(frm, frappe.datetime.now_datetime(), frm.doc.employee);
frm.events.start_timer(frm, from_time, frm.doc.employee);
}
});
}

View File

@@ -485,4 +485,3 @@ erpnext.patches.v16_0.clear_procedures_from_receivable_report
erpnext.patches.v16_0.migrate_address_contact_custom_fields
erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb
execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600)
erpnext.patches.v16_0.remove_mandatory_from_inv_dimension_fields

View File

@@ -9,6 +9,7 @@ def get_inventory_dimensions():
"source_fieldname",
"reference_document as doctype",
"reqd",
"mandatory_depends_on",
],
order_by="creation",
distinct=True,
@@ -84,5 +85,5 @@ def execute():
"Custom Field",
{"fieldname": fieldname, "dt": dimension.doctype},
"mandatory_depends_on",
display_depends_on if dimension.reqd else "",
display_depends_on if dimension.reqd else dimension.mandatory_depends_on,
)

View File

@@ -1,54 +0,0 @@
import frappe
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_documents
def execute():
"""Mandatory inventory dimensions are now enforced on the server side
(StockController.validate_inventory_dimension_mandatory) instead of via field-level
`reqd`/`mandatory_depends_on`. Clear those properties from the related custom fields."""
dimensions = frappe.get_all(
"Inventory Dimension",
fields=[
"source_fieldname",
"reference_document",
"document_type",
"apply_to_all_doctypes",
],
)
for dimension in dimensions:
if not dimension.source_fieldname or not dimension.reference_document:
continue
# Scope to the exact doctypes where this dimension generated fields so unrelated
# mandatory custom fields (same name/target on a different doctype) are never touched.
if dimension.apply_to_all_doctypes:
doctypes = [d[0] for d in get_inventory_documents()]
elif dimension.document_type:
doctypes = [dimension.document_type]
else:
continue
fieldname = dimension.source_fieldname
fieldnames = [fieldname, f"to_{fieldname}", f"from_{fieldname}", f"rejected_{fieldname}"]
custom_fields = frappe.get_all(
"Custom Field",
filters={
"dt": ("in", doctypes),
"fieldname": ("in", fieldnames),
"fieldtype": "Link",
"options": dimension.reference_document,
},
or_filters={"reqd": 1, "mandatory_depends_on": ("is", "set")},
pluck="name",
)
for name in custom_fields:
frappe.db.set_value(
"Custom Field",
name,
{"reqd": 0, "mandatory_depends_on": ""},
update_modified=False,
)

View File

@@ -23,12 +23,15 @@ erpnext.accounts.taxes = {
onload: function (frm) {
if (frm.get_field("taxes")) {
frm.set_query("account_head", "taxes", function (doc) {
let account_type = ["Tax", "Chargeable"];
if (frm.cscript.tax_table == "Sales Taxes and Charges") {
account_type.push("Expense Account");
var account_type = ["Tax", "Chargeable", "Expense Account"];
} else {
account_type.push("Income Account", "Expenses Included In Valuation");
var account_type = [
"Tax",
"Chargeable",
"Income Account",
"Expenses Included In Valuation",
];
}
return {

View File

@@ -10,30 +10,29 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
apply_pricing_rule_on_item(item) {
let effective_item_rate = item.price_list_rate;
let item_rate = item.rate;
if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) {
effective_item_rate = item.blanket_order_rate;
}
let rate_with_margin;
if (item.margin_type == "Percentage") {
rate_with_margin = effective_item_rate * (1 + item.margin_rate_or_amount / 100);
item.rate_with_margin =
flt(effective_item_rate) + flt(effective_item_rate) * (flt(item.margin_rate_or_amount) / 100);
} else {
rate_with_margin = effective_item_rate + item.margin_rate_or_amount;
item.rate_with_margin = flt(effective_item_rate) + flt(item.margin_rate_or_amount);
}
item.rate_with_margin = flt(rate_with_margin, precision("rate_with_margin", item));
item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate);
if (item.discount_percentage) {
item.discount_amount = flt(
(item.rate_with_margin * item.discount_percentage) / 100,
precision("discount_amount", item)
);
item_rate = flt(item.rate_with_margin, precision("rate", item));
if (item.discount_percentage && !item.discount_amount) {
item.discount_amount = (flt(item.rate_with_margin) * flt(item.discount_percentage)) / 100;
}
let item_rate = item.rate_with_margin;
if (item.discount_amount) {
item_rate = item.rate_with_margin - item.discount_amount;
if (item.discount_amount > 0) {
item_rate = flt(item.rate_with_margin - item.discount_amount, precision("rate", item));
item.discount_percentage = (100 * flt(item.discount_amount)) / flt(item.rate_with_margin);
}
item_rate = flt(item_rate, precision("rate", item));
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
}
@@ -952,15 +951,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,
@@ -1005,15 +1003,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
async set_total_amount_to_default_mop() {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,

View File

@@ -13,58 +13,39 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function (frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
var has_margin_field = frappe.meta.has_field(cdt, "margin_type");
frappe.model.round_floats_in(item, [
"rate",
"price_list_rate",
"margin_rate_or_amount",
"discount_amount",
"discount_percentage",
]);
frappe.model.round_floats_in(item, ["rate", "price_list_rate"]);
if (item.price_list_rate && !item.blanket_order_rate) {
const rate_with_margin = get_rate_with_margin(item);
if (item.discount_percentage) {
item.discount_amount = flt(
(rate_with_margin * item.discount_percentage) / 100.0,
precision("discount_amount", item)
);
}
const calculated_rate = flt(rate_with_margin - item.discount_amount, precision("rate", item));
if (calculated_rate !== item.rate) {
if (item.rate > item.price_list_rate && has_margin_field) {
// if rate is greater than price_list_rate, set margin
// otherwise, set discount
if (item.rate > item.price_list_rate) {
item.margin_type = "Amount";
item.margin_rate_or_amount = flt(
item.rate - item.price_list_rate,
precision("margin_rate_or_amount", item)
);
item.rate_with_margin = item.rate;
item.discount_amount = 0;
item.discount_percentage = 0;
} else {
item.margin_type = "";
item.margin_rate_or_amount = 0;
item.rate_with_margin = item.price_list_rate;
item.discount_percentage = 0;
item.discount_amount = flt(
item.rate_with_margin - item.rate,
precision("discount_amount", item)
);
}
// or set discount
item.discount_percentage = 0;
item.margin_type = "Amount";
item.margin_rate_or_amount = flt(
item.rate - item.price_list_rate,
precision("margin_rate_or_amount", item)
);
item.rate_with_margin = item.rate;
} else {
item.discount_percentage = flt(
(1 - item.rate / item.price_list_rate) * 100.0,
precision("discount_percentage", item)
);
item.discount_amount = flt(item.price_list_rate) - flt(item.rate);
item.margin_type = "";
item.margin_rate_or_amount = 0;
item.rate_with_margin = 0;
}
} else {
item.discount_percentage = 0.0;
item.margin_type = "";
item.margin_rate_or_amount = 0;
item.rate_with_margin = 0;
item.discount_amount = 0;
item.discount_percentage = 0.0;
}
me.set_in_company_currency(item, ["rate_with_margin"]);
item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate);
cur_frm.cscript.set_gross_profit(item);
cur_frm.cscript.calculate_taxes_and_totals();
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
@@ -1273,8 +1254,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var set_party_account = function (set_pricing) {
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
let party_type = me.frm.doc.doctype == "Sales Invoice" ? "Customer" : "Supplier";
let party_account_field = me.frm.doc.doctype == "Sales Invoice" ? "debit_to" : "credit_to";
if (me.frm.doc.doctype == "Sales Invoice") {
var party_type = "Customer";
var party_account_field = "debit_to";
} else {
var party_type = "Supplier";
var party_account_field = "credit_to";
}
var party = me.frm.doc[frappe.model.scrub(party_type)];
if (
@@ -1981,7 +1967,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
let item_grid = this.frm.fields_dict["operations"].grid;
var item_grid = this.frm.fields_dict["operations"].grid;
$.each(["base_operating_cost", "base_hour_rate"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -1989,7 +1975,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
let item_grid = this.frm.fields_dict["secondary_items"].grid;
var item_grid = this.frm.fields_dict["secondary_items"].grid;
$.each(["base_rate", "base_amount"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -2386,7 +2372,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
row_to_modify[key] = pr_row[key];
}
if (Object.prototype.hasOwnProperty.call(this.frm.doc, "is_pos") && this.frm.doc.is_pos) {
if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center");
if (r.message.cost_center) {
row_to_modify["cost_center"] = r.message.cost_center;
@@ -2653,7 +2639,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
$.each(me.frm.doc.items || [], function (i, item) {
if (
item.name &&
Object.prototype.hasOwnProperty.call(r.message, item.name) &&
r.message.hasOwnProperty(item.name) &&
r.message[item.name].item_tax_template
) {
item.item_tax_template = r.message[item.name].item_tax_template;
@@ -3377,13 +3363,3 @@ erpnext.set_unit_price_items_note = (frm) => {
);
}
};
function get_rate_with_margin(item) {
if (!item.margin_type) return item.price_list_rate;
if (item.margin_type === "Percentage") {
return flt(item.price_list_rate * (1 + item.margin_rate_or_amount / 100), precision("rate", item));
}
return flt(item.price_list_rate + item.margin_rate_or_amount, precision("rate", item));
}

View File

@@ -188,19 +188,11 @@ $.extend(erpnext.utils, {
]),
"blue"
);
var info = company_wise_info[0];
var is_advance = info.balance_label !== "Total Unpaid";
var indicator_label =
info.balance_label === "Total Advance Paid"
? __("Total Advance Paid: {0}", [format_currency(info.balance_amount, info.currency)])
: info.balance_label === "Total Advance Received"
? __("Total Advance Received: {0}", [
format_currency(info.balance_amount, info.currency),
])
: __("Total Unpaid: {0}", [format_currency(info.balance_amount, info.currency)]);
frm.dashboard.add_indicator(
indicator_label,
is_advance ? "green" : info.balance_amount ? "orange" : "green"
__("Total Unpaid: {0}", [
format_currency(company_wise_info[0].total_unpaid, company_wise_info[0].currency),
]),
company_wise_info[0].total_unpaid ? "orange" : "green"
);
if (company_wise_info[0].loyalty_points) {
@@ -243,14 +235,7 @@ $.extend(erpnext.utils, {
frm.dashboard.stats_area_row.addClass("flex");
frm.dashboard.stats_area_row.css("flex-wrap", "wrap");
var is_advance = info.balance_label !== "Total Unpaid";
var color = is_advance ? "green" : info.balance_amount ? "orange" : "green";
var balance_label_text =
info.balance_label === "Total Advance Paid"
? __("Total Advance Paid")
: info.balance_label === "Total Advance Received"
? __("Total Advance Received")
: __("Total Unpaid");
var color = info.total_unpaid ? "orange" : "green";
var indicator = $(
'<div class="flex-column col-xs-6">' +
@@ -264,10 +249,8 @@ $.extend(erpnext.utils, {
'<div class="badge-link small" style="margin-bottom:10px">' +
'<span class="indicator ' +
color +
'">' +
balance_label_text +
": " +
format_currency(info.balance_amount, info.currency) +
'">Total Unpaid: ' +
format_currency(info.total_unpaid, info.currency) +
"</span></div>" +
"</div>"
).appendTo(frm.dashboard.stats_area_row);

View File

@@ -16,7 +16,7 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestVATAuditReport(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company SA VAT"
make_company("_Test Company SA VAT", "_TCSV")
create_account(
account_name="VAT - 0%",

View File

@@ -130,7 +130,6 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
},
{
@@ -696,7 +695,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-27 16:12:10.457900",
"modified": "2026-06-22 12:23:19.196991",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -403,9 +403,9 @@ class TestQuotation(ERPNextTestSuite):
quotation.save()
quotation.submit()
self.assertEqual(quotation.payment_schedule[0].payment_amount, 500.00)
self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
self.assertEqual(quotation.payment_schedule[1].payment_amount, 500.00)
self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00)
self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30))
sales_order = make_sales_order(quotation.name)
@@ -425,11 +425,11 @@ class TestQuotation(ERPNextTestSuite):
sales_order.set("taxes", [])
sales_order.save()
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 500.00)
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(
getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date)
)
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 500.00)
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
self.assertEqual(
getdate(sales_order.payment_schedule[1].due_date),
getdate(add_days(quotation.transaction_date, 30)),
@@ -465,13 +465,11 @@ class TestQuotation(ERPNextTestSuite):
rate_with_margin = flt((1500 * 18.75) / 100 + 1500)
test_record = frappe.copy_doc(self.globalTestRecords["Quotation"][0])
test_record = dict(self.globalTestRecords["Quotation"][0])
test_record.items[0].price_list_rate = 1500
test_record.items[0].margin_type = "Percentage"
test_record.items[0].margin_rate_or_amount = 18.75
# set rate to zero, so that it is recalculated on save
test_record.items[0].rate = 0
test_record["items"][0]["price_list_rate"] = 1500
test_record["items"][0]["margin_type"] = "Percentage"
test_record["items"][0]["margin_rate_or_amount"] = 18.75
quotation = frappe.copy_doc(test_record)
quotation.transaction_date = nowdate()

View File

@@ -1473,8 +1473,6 @@ class TestSalesOrder(ERPNextTestSuite):
so.items[0].price_list_rate = price_list_rate = 100
so.items[0].margin_type = "Percentage"
so.items[0].margin_rate_or_amount = 25
# set rate to zero, so that it is recalculated on save
so.items[0].rate = 0
so.save()
new_so = frappe.copy_doc(so)

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, msgprint, qb
from frappe.query_builder import Case, Criterion
from frappe.query_builder import Criterion
from erpnext import get_company_currency
@@ -155,60 +155,50 @@ def get_columns(filters):
def get_entries(filters):
doc_type = filters["doc_type"]
date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date"
if filters["doc_type"] == "Sales Order":
qty_field = "delivered_qty"
else:
qty_field = "qty"
conditions, values = get_conditions(filters, date_field)
date_field = "transaction_date" if doc_type == "Sales Order" else "posting_date"
qty_field = "delivered_qty" if doc_type == "Sales Order" else "qty"
dt = frappe.qb.DocType(doc_type)
dt_item = frappe.qb.DocType(f"{doc_type} Item")
st = frappe.qb.DocType("Sales Team")
calc_qty = dt_item[qty_field] * dt_item.conversion_factor
calc_net_amount = dt_item.base_net_rate * calc_qty
stock_qty_case = Case().when(dt.status == "Closed", calc_qty).else_(dt_item.stock_qty).as_("stock_qty")
base_net_amount_case = (
Case()
.when(dt.status == "Closed", calc_net_amount)
.else_(dt_item.base_net_amount)
.as_("base_net_amount")
entries = frappe.db.sql(
"""
SELECT
dt.name, dt.customer, dt.territory, dt.{} as posting_date, dt_item.item_code,
st.sales_person, st.allocated_percentage, dt_item.warehouse,
CASE
WHEN dt.status = "Closed" THEN dt_item.{} * dt_item.conversion_factor
ELSE dt_item.stock_qty
END as stock_qty,
CASE
WHEN dt.status = "Closed" THEN (dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor)
ELSE dt_item.base_net_amount
END as base_net_amount,
CASE
WHEN dt.status = "Closed" THEN ((dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) * st.allocated_percentage/100)
ELSE dt_item.base_net_amount * st.allocated_percentage/100
END as contribution_amt
FROM
`tab{}` dt, `tab{} Item` dt_item, `tabSales Team` st
WHERE
st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = {}
and dt.docstatus = 1 {} order by st.sales_person, dt.name desc
""".format(
date_field,
qty_field,
qty_field,
qty_field,
filters["doc_type"],
filters["doc_type"],
"%s",
conditions,
),
tuple([filters["doc_type"], *values]),
as_dict=1,
)
contribution_amt_case = (
Case()
.when(dt.status == "Closed", (calc_net_amount * st.allocated_percentage / 100))
.else_(dt_item.base_net_amount * st.allocated_percentage / 100)
.as_("contribution_amt")
)
query = (
frappe.get_query(dt, filters=filters, ignore_permissions=False)
.join(dt_item)
.on(dt.name == dt_item.parent)
.join(st)
.on(dt.name == st.parent)
.select(
dt.name,
dt.customer,
dt.territory,
dt[date_field].as_("posting_date"),
dt_item.item_code,
st.sales_person,
st.allocated_percentage,
dt_item.warehouse,
stock_qty_case,
base_net_amount_case,
contribution_amt_case,
)
.where(st.parenttype == doc_type)
.where(dt.docstatus == 1)
)
query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc)
return query.run(as_dict=True)
return entries
def get_conditions(filters, date_field):

View File

@@ -214,14 +214,5 @@
"doctype": "Company",
"chart_of_accounts": "Standard",
"create_chart_of_accounts_based_on": "Standard Template"
},
{
"abbr": "TPC",
"company_name": "Test PCV Company",
"country": "United States",
"default_currency": "USD",
"doctype": "Company",
"chart_of_accounts": "Standard",
"create_chart_of_accounts_based_on": "Standard Template"
}
]

View File

@@ -318,23 +318,12 @@ class TransactionDeletionRecord(Document):
Returns:
list: List of child table DocType names (Table field options)
"""
child_tables = frappe.get_all(
return frappe.get_all(
"DocField",
filters={"parent": doctype_name, "fieldtype": ["in", ["Table", "Table MultiSelect"]]},
pluck="options",
)
if not child_tables:
return []
child_tables = frappe.get_all(
"DocType",
filters={"name": ["in", child_tables], "is_virtual": 0},
pluck="name",
)
return child_tables
def _get_to_delete_row_infos(self, doctype_name, company_field=None, company=None):
"""Get child tables and document count for a To Delete list row

View File

@@ -51,6 +51,7 @@ frappe.ui.form.on("Inventory Dimension", {
"fetch_from_parent",
"type_of_transaction",
"condition",
"mandatory_depends_on",
"validate_negative_stock",
];

View File

@@ -27,7 +27,7 @@
"condition",
"conditional_mandatory_section",
"reqd",
"mandatory_depends_on_backend",
"mandatory_depends_on",
"conditional_rule_examples_section",
"html_19"
],
@@ -151,6 +151,13 @@
"fieldtype": "Section Break",
"label": "Conditional Rule Examples"
},
{
"depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
"fieldname": "mandatory_depends_on",
"fieldtype": "Small Text",
"label": "Mandatory Depends On"
},
{
"fieldname": "conditional_mandatory_section",
"fieldtype": "Section Break",
@@ -162,13 +169,6 @@
"fieldtype": "Check",
"label": "Mandatory"
},
{
"depends_on": "eval:!doc.reqd",
"description": "Python expression evaluated on the server. Use doc.fieldname for the row and parent.fieldname for the parent document. When it evaluates to true the dimension becomes mandatory. Example: doc.t_warehouse and doc.qty > 0",
"fieldname": "mandatory_depends_on_backend",
"fieldtype": "Small Text",
"label": "Mandatory Depends On (Backend)"
},
{
"fieldname": "column_break_niy2u",
"fieldtype": "Column Break"
@@ -182,7 +182,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-25 11:30:00.000000",
"modified": "2026-04-08 10:10:16.884388",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

View File

@@ -35,7 +35,7 @@ class InventoryDimension(Document):
document_type: DF.Link | None
fetch_from_parent: DF.Literal[None]
istable: DF.Check
mandatory_depends_on_backend: DF.SmallText | None
mandatory_depends_on: DF.SmallText | None
reference_document: DF.Link
reqd: DF.Check
source_fieldname: DF.Data | None
@@ -118,6 +118,7 @@ class InventoryDimension(Document):
def reset_value(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = ""
self.mandatory_depends_on = ""
self.istable = 0
for field in ["document_type", "condition"]:
@@ -166,10 +167,15 @@ class InventoryDimension(Document):
if label_start_with:
label = f"{label_start_with} {self.dimension_name}"
# Note: `reqd` is intentionally NOT set on the custom fields. Mandatory enforcement
# happens on the server side via StockController.validate_inventory_dimension_mandatory()
# so that it can be gated (e.g. skip service rows) and never blocks documents that are
# being cancelled.
mandatory_depends_on = self.mandatory_depends_on
if self.reqd:
if doctype == "Stock Entry Detail":
mandatory_depends_on = "eval:doc.s_warehouse"
elif doctype == "Subcontracting Receipt Supplied Item":
mandatory_depends_on = "eval:doc.reference_name"
elif doctype == "Packed Item":
mandatory_depends_on = "eval:doc.parent_detail_docname && ['Delivery Note', 'Sales Invoice', 'POS Invoice'].includes(parent.doctype)"
dimension_fields = [
dict(
fieldname="inventory_dimension",
@@ -186,6 +192,13 @@ class InventoryDimension(Document):
label=_(label),
depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "",
search_index=1,
reqd=1
if self.reqd
and not self.mandatory_depends_on
and doctype
not in ["Stock Entry Detail", "Subcontracting Receipt Supplied Item", "Packed Item"]
else 0,
mandatory_depends_on=mandatory_depends_on,
),
]
@@ -198,6 +211,7 @@ class InventoryDimension(Document):
options=self.reference_document,
label=_("Rejected " + self.dimension_name),
search_index=1,
mandatory_depends_on="eval:doc.rejected_qty > 0",
)
)
@@ -224,7 +238,9 @@ class InventoryDimension(Document):
and not frappe.db.get_value("Custom Field", {"dt": dt, "fieldname": self.target_fieldname})
and not field_exists(dt, self.target_fieldname)
):
dimension_field = dimension_fields[1].copy()
dimension_field = dimension_fields[1]
dimension_field["mandatory_depends_on"] = ""
dimension_field["reqd"] = 0
dimension_field["fieldname"] = self.target_fieldname
custom_fields[dt] = dimension_field
@@ -293,6 +309,7 @@ class InventoryDimension(Document):
options=self.reference_document,
label=label,
depends_on=display_depends_on,
mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on,
),
]
)
@@ -373,101 +390,6 @@ def get_document_wise_inventory_dimensions(doctype) -> dict:
)
@request_cache
def get_mandatory_inventory_dimensions(doctype) -> list:
"""Return the inventory dimensions applicable to `doctype` (a child doctype such as
`Stock Entry Detail`) that need server-side mandatory enforcement.
A dimension qualifies only if it is configured as mandatory (`reqd`) or has a server-side
mandatory condition (`mandatory_depends_on_backend`). Non-mandatory dimensions are never
enforced, including the rejected dimension field on purchase rows."""
dimensions = frappe.get_all(
"Inventory Dimension",
fields=[
"name",
"dimension_name",
"source_fieldname",
"reqd",
"mandatory_depends_on_backend",
],
or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
)
return [d for d in dimensions if d.reqd or d.mandatory_depends_on_backend]
def get_mandatory_dimension_fields(doctype, dimension) -> list:
"""For a mandatory `dimension` return the list of (fieldname, condition) tuples that must be
filled on a row of `doctype`. `condition` is a python expression evaluated with `doc` (the row)
and `parent`; a `None` condition means the field is unconditionally mandatory.
Mirrors the mandatory logic that used to live on the custom fields in `get_dimension_fields`."""
fields = []
source_fieldname = dimension.source_fieldname
# `mandatory_depends_on_backend` is a raw python expression evaluated server-side
# (with `doc` and `parent`), so it can be used as a condition directly.
backend_condition = (dimension.mandatory_depends_on_backend or "").strip() or None
# Primary source dimension field
if dimension.reqd and doctype == "Stock Entry Detail":
fields.append((source_fieldname, "doc.s_warehouse"))
elif dimension.reqd and doctype == "Subcontracting Receipt Supplied Item":
fields.append((source_fieldname, "doc.reference_name"))
elif dimension.reqd and doctype == "Packed Item":
fields.append(
(
source_fieldname,
"doc.parent_detail_docname and parent.doctype in ['Delivery Note', 'Sales Invoice', 'POS Invoice']",
)
)
elif dimension.reqd:
fields.append((source_fieldname, None))
elif backend_condition:
fields.append((source_fieldname, backend_condition))
# Rejected dimension field (only present on purchase rows). Enforced only when the dimension
# is mandatory for the row AND there is a rejected quantity.
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
if dimension.reqd:
fields.append((f"rejected_{source_fieldname}", "doc.rejected_qty > 0"))
elif backend_condition:
fields.append((f"rejected_{source_fieldname}", f"({backend_condition}) and doc.rejected_qty > 0"))
# Target/transfer dimension field used for internal transfers (mirrors the old
# `add_transfer_field` behaviour). When the dimension is `reqd` the field inherits the
# transfer display condition, otherwise it inherits the server-side mandatory condition.
if (dimension.reqd or backend_condition) and doctype in [
"Stock Entry Detail",
"Sales Invoice Item",
"Delivery Note Item",
"Purchase Invoice Item",
"Purchase Receipt Item",
]:
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
transfer_fieldname, display_condition = (
f"from_{source_fieldname}",
"parent.is_internal_supplier == 1",
)
elif doctype == "Stock Entry Detail":
transfer_fieldname, display_condition = f"to_{source_fieldname}", "doc.t_warehouse"
else:
transfer_fieldname, display_condition = (
f"to_{source_fieldname}",
"parent.is_internal_customer == 1",
)
# The transfer field only applies to internal transfers, so its mandatory check is always
# gated on the display condition; the backend condition narrows it further.
if dimension.reqd:
transfer_condition = display_condition
else:
transfer_condition = f"({display_condition}) and ({backend_condition})"
fields.append((transfer_fieldname, transfer_condition))
return fields
@frappe.whitelist()
@request_cache
def get_inventory_dimensions():

View File

@@ -219,75 +219,35 @@ class TestInventoryDimension(ERPNextTestSuite):
doc.reqd = 1
doc.save()
# Mandatory enforcement is now done server-side, so the custom field must NOT be `reqd`.
self.assertFalse(
self.assertTrue(
frappe.db.get_value(
"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item"}, "reqd"
"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name"
)
)
item_code = "Test Mandatory Dimension Item"
create_item(item_code)
warehouse = create_warehouse("Mandatory Dimension Warehouse")
dn_doc = create_delivery_note(item_code=item_code, qty=5, warehouse=warehouse, do_not_save=True)
# Dimension value missing -> server-side validation should block the document.
self.assertRaises(frappe.ValidationError, dn_doc.save)
if not frappe.db.exists("Pallet", "Pallet 75 Value"):
frappe.get_doc({"doctype": "Pallet", "pallet_name": "Pallet 75 Value"}).insert(
ignore_permissions=True
)
dn_doc.items[0].pallet_75 = "Pallet 75 Value"
dn_doc.save()
doc.reqd = 0
doc.save()
def test_check_mandatory_depends_on_backend(self):
def test_check_mandatory_depends_on_dimensions(self):
doc = create_inventory_dimension(
reference_document="Pallet",
type_of_transaction="Outward",
dimension_name="Pallet Backend",
dimension_name="Pallet",
apply_to_all_doctypes=0,
document_type="Delivery Note Item",
document_type="Stock Entry Detail",
)
doc.reqd = 0
doc.mandatory_depends_on_backend = "doc.qty > 0"
doc.mandatory_depends_on = "t_warehouse"
doc.save()
# The condition is enforced server-side, the custom field must not carry field-level `reqd`.
self.assertFalse(
self.assertTrue(
frappe.db.get_value(
"Custom Field", {"fieldname": "pallet_backend", "dt": "Delivery Note Item"}, "reqd"
"Custom Field",
{"fieldname": "pallet", "dt": "Stock Entry Detail", "mandatory_depends_on": "t_warehouse"},
"name",
)
)
item_code = "Test Backend Dimension Item"
create_item(item_code)
warehouse = create_warehouse("Backend Dimension Warehouse")
dn_doc = create_delivery_note(item_code=item_code, qty=5, warehouse=warehouse, do_not_save=True)
# qty > 0 -> backend condition is met, so the dimension is mandatory and blocks the save.
self.assertRaises(frappe.ValidationError, dn_doc.save)
if not frappe.db.exists("Pallet", "Pallet Backend Value"):
frappe.get_doc({"doctype": "Pallet", "pallet_name": "Pallet Backend Value"}).insert(
ignore_permissions=True
)
dn_doc.items[0].pallet_backend = "Pallet Backend Value"
dn_doc.save()
# Reset so the always-true condition does not make the dimension mandatory for
# subsequent Delivery Note tests sharing the same test database.
doc.mandatory_depends_on_backend = ""
doc.save()
def test_for_purchase_sales_and_stock_transaction(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:parameter",
"creation": "2020-12-28 17:06:00.254129",
"doctype": "DocType",
@@ -35,7 +34,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-19 10:55:00.000000",
"modified": "2024-03-27 13:10:28.861722",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Parameter",

View File

@@ -93,7 +93,7 @@ class RepostItemValuation(Document):
self.validate_recreate_stock_ledgers()
def set_default_posting_time(self):
if self.posting_time is None:
if not self.posting_time:
self.posting_time = nowtime()
if not self.posting_date:
@@ -346,9 +346,6 @@ class RepostItemValuation(Document):
def _recalculate_valuation_rate(self):
doc = frappe.get_doc(self.voucher_type, self.voucher_no)
if doc.get("is_internal_supplier"):
doc.set_sales_incoming_rate_for_internal_transfer()
doc.update_valuation_rate()
for item in doc.items:
item.db_set("valuation_rate", item.valuation_rate)

View File

@@ -292,9 +292,6 @@ class StockEntry(StockController, SubcontractingInwardController):
self.validate_putaway_capacity()
self.validate_component_and_quantities()
self.validate_finished_good_serial_batch_for_work_order()
# Stock Entry overrides validate() without calling super(), so the shared mandatory
# inventory dimension check must be invoked explicitly here.
self.validate_inventory_dimension_mandatory()
if self.get("purpose") != "Manufacture":
# ignore other item wh difference and empty source/target wh

View File

@@ -1244,107 +1244,6 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
self.assertEqual(sle[0].qty_after_transaction, 105)
self.assertEqual(sle[0].actual_qty, 100)
def test_update_qty_in_future_sle_shifts_same_timestamp_later_entry(self):
# update_qty_in_future_sle treats "future" as strictly after the current entry in the
# (posting_datetime, creation) order. An entry sharing the exact posting timestamp but created
# later must still have its running balance shifted; comparing posting_datetime alone would skip
# it. The current entry itself (same timestamp, same creation) must not be shifted.
from erpnext.stock.stock_ledger import update_qty_in_future_sle
item = make_item().name
warehouse = "_Test Warehouse - _TC"
receipt1 = make_purchase_receipt(
item_code=item,
warehouse=warehouse,
qty=10,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
)
time.sleep(1)
receipt2 = make_purchase_receipt(
item_code=item,
warehouse=warehouse,
qty=20,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00", # identical timestamp, later creation
)
def sle(voucher):
return frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": voucher.name, "is_cancelled": 0},
["name", "posting_date", "posting_time", "creation", "qty_after_transaction"],
as_dict=True,
)
sle1, sle2 = sle(receipt1), sle(receipt2)
self.assertEqual(sle1.qty_after_transaction, 10)
self.assertEqual(sle2.qty_after_transaction, 30)
# Simulate a +5 qty shift originating at receipt1's ledger position.
args = frappe._dict(
{
"item_code": item,
"warehouse": warehouse,
"voucher_type": "Purchase Receipt",
"voucher_no": receipt1.name,
"posting_date": sle1.posting_date,
"posting_time": sle1.posting_time,
"creation": sle1.creation,
"actual_qty": 5,
}
)
update_qty_in_future_sle(args, allow_negative_stock=True)
# receipt2 (same timestamp, later creation) is shifted; receipt1 (the current entry) is not.
self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle2.name, "qty_after_transaction"), 35)
self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle1.name, "qty_after_transaction"), 10)
def test_get_next_stock_reco_respects_creation_order(self):
# A stock reco sharing the exact posting timestamp of the current entry must only count as the
# "next" reco when it was created after that entry. A reco created before it actually precedes
# the entry and must not bound (truncate) the qty-shift range.
from erpnext.stock.stock_ledger import get_next_stock_reco
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reco = create_stock_reconciliation(
item_code=item,
warehouse=warehouse,
qty=10,
rate=100,
posting_date="2021-01-01",
posting_time="02:00:00",
)
reco_sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": reco.name, "is_cancelled": 0},
["posting_date", "posting_time", "creation"],
as_dict=True,
)
base_kwargs = {
"item_code": item,
"warehouse": warehouse,
"voucher_no": "SOME-OTHER-VOUCHER",
"posting_date": reco_sle.posting_date,
"posting_time": reco_sle.posting_time,
}
# Current entry created AFTER the reco at the same timestamp -> reco precedes it -> not returned.
after = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=5)}
self.assertFalse(get_next_stock_reco(after))
# Current entry created BEFORE the reco at the same timestamp -> reco follows it -> returned.
before = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=-5)}
result = get_next_stock_reco(before)
self.assertTrue(result)
self.assertEqual(result[0].voucher_no, reco.name)
@ERPNextTestSuite.change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
def test_transfer_invariants(self):
"""Extact stock value should be transferred."""

View File

@@ -83,9 +83,6 @@ class StockReconciliation(StockController):
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
self.validate_inventory_dimension()
# Stock Reconciliation overrides validate() without calling super(), so the shared
# mandatory inventory dimension check must be invoked explicitly here.
self.validate_inventory_dimension_mandatory()
self.validate_uom_is_integer("stock_uom", "qty")
if self._action == "submit":
@@ -1008,102 +1005,6 @@ class StockReconciliation(StockController):
d.quantity_difference = flt(d.qty) - flt(d.current_qty)
d.amount_difference = flt(d.amount) - flt(d.current_amount)
def recalculate_difference_amount_from_ledger(self):
"""Sync the displayed current qty/rate and difference amount with the (reposted) ledger.
Submitted reconciliations freeze ``difference_amount`` and the per-row current values at
submit time, but reposting/backdated transactions recompute the reconciliation's Stock Ledger
Entries and rebuild the GL from them. Without this sync the document keeps showing stale figures
that no longer match the GL entries. Anchoring ``amount_difference`` to the row's summed
``stock_value_difference`` keeps the document and the GL consistent by construction.
"""
difference_amount = 0.0
for row in self.items:
stock_value_difference = flt(get_row_stock_value_difference(self.doctype, self.name, row.name))
amount = flt(flt(row.qty) * flt(row.valuation_rate), row.precision("amount"))
amount_difference = flt(stock_value_difference, row.precision("amount_difference"))
current_amount = flt(amount - amount_difference, row.precision("current_amount"))
current_qty = self.get_current_qty_from_ledger(row)
current_valuation_rate = (
flt(current_amount / current_qty, row.precision("current_valuation_rate"))
if current_qty
else 0.0
)
row.db_set(
{
"amount": amount,
"current_qty": current_qty,
"current_valuation_rate": current_valuation_rate,
"current_amount": current_amount,
"quantity_difference": flt(row.qty) - current_qty,
"amount_difference": amount_difference,
},
update_modified=False,
)
difference_amount += amount_difference
self.db_set(
"difference_amount",
flt(difference_amount, self.precision("difference_amount")),
update_modified=False,
)
def get_current_qty_from_ledger(self, row: StockReconciliationItem):
"""Current (pre-reconciliation) qty for a row, recomputed from the ledger after reposting.
Serial/batch rows cannot have backdated qty changes inserted before a future reconciliation
(blocked by ``check_future_entries_exists``), so their current qty is frozen and read straight
from the current bundle. Non-serial rows can float, so read the ledger balance just before the
reconciliation, excluding the reconciliation's own entries.
"""
if row.current_serial_and_batch_bundle:
total_qty = frappe.db.get_value(
"Serial and Batch Bundle", row.current_serial_and_batch_bundle, "total_qty"
)
return abs(flt(total_qty, row.precision("current_qty")))
reco_sle = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"is_cancelled": 0,
},
["posting_datetime", "creation"],
as_dict=True,
)
if not reco_sle:
return flt(row.current_qty, row.precision("current_qty"))
sle = frappe.qb.DocType("Stock Ledger Entry")
previous_sle = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)
.where(
(sle.item_code == row.item_code)
& (sle.warehouse == row.warehouse)
& (sle.is_cancelled == 0)
& (
(sle.posting_datetime < reco_sle.posting_datetime)
| (
(sle.posting_datetime == reco_sle.posting_datetime)
& (sle.creation < reco_sle.creation)
)
)
)
.orderby(sle.posting_datetime, order=frappe.qb.desc)
.orderby(sle.creation, order=frappe.qb.desc)
.limit(1)
).run()
return flt(previous_sle[0][0], row.precision("current_qty")) if previous_sle else 0.0
def submit(self):
if len(self.items) > 100:
msgprint(
@@ -1290,23 +1191,6 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
return itemwise_batch_data
def get_row_stock_value_difference(voucher_type: str, voucher_no: str, voucher_detail_no: str):
"""Net stock value change posted to the GL by a reconciliation row (sum of its SLEs)."""
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Sum(sle.stock_value_difference))
.where(
(sle.voucher_type == voucher_type)
& (sle.voucher_no == voucher_no)
& (sle.voucher_detail_no == voucher_detail_no)
& (sle.is_cancelled == 0)
)
).run()
return flt(result[0][0]) if result and result[0][0] else 0.0
@frappe.whitelist()
def get_stock_balance_for(
item_code: str,

View File

@@ -787,172 +787,6 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
sr1.load_from_db()
self.assertEqual(sr1.difference_amount, 10000)
def assert_reco_difference_matches_gl(self, reco_name):
"""The displayed Difference Amount (doc and per-row) must equal the reposted GL impact,
i.e. the sum of the reconciliation's Stock Ledger Entry ``stock_value_difference``."""
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
get_row_stock_value_difference,
)
reco = frappe.get_doc("Stock Reconciliation", reco_name)
total_difference = 0.0
for row in reco.items:
row_difference = flt(
get_row_stock_value_difference("Stock Reconciliation", reco_name, row.name),
row.precision("amount_difference"),
)
self.assertEqual(flt(row.amount_difference), row_difference)
total_difference += row_difference
self.assertEqual(
flt(reco.difference_amount, reco.precision("difference_amount")),
flt(total_difference, reco.precision("difference_amount")),
)
def test_difference_amount_synced_with_gl_after_repost_non_serialized(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = self.make_item().name
warehouse = "_Test Warehouse - _TC"
# Opening stock => 100 * 100 = 10000
make_stock_entry(
item_code=item_code,
target=warehouse,
qty=100,
basic_rate=100,
posting_date=add_days(nowdate(), -5),
posting_time="10:00:00",
)
# Reconcile to 100 @ 200 => difference 20000 - 10000 = 10000
reco = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=100,
rate=200,
posting_date=add_days(nowdate(), -2),
)
self.assertEqual(reco.difference_amount, 10000)
self.assert_reco_difference_matches_gl(reco.name)
# Backdated reconciliation lowers the pre-reco stock value to 50 * 50 = 2500
create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=50,
rate=50,
posting_date=add_days(nowdate(), -3),
)
reco.load_from_db()
# Current is now 2500 => difference 20000 - 2500 = 17500
self.assertEqual(reco.difference_amount, 17500)
self.assert_reco_difference_matches_gl(reco.name)
def test_difference_amount_synced_with_gl_after_repost_batched(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
make_landed_cost_voucher,
)
item_code = self.make_item(
"Test Batch Item Reco Difference Sync",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TEST-BATCH-DIFFSYNC-.###",
},
).name
warehouse = "_Test Warehouse - _TC"
# Receive 10 @ 100 (batch value 1000)
pr = make_purchase_receipt(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=100,
posting_date=add_days(nowdate(), -5),
)
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
# Reconcile the batch to 10 @ 500 => difference 5000 - 1000 = 4000
reco = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=500,
batch_no=batch_no,
use_serial_batch_fields=1,
posting_date=add_days(nowdate(), -2),
)
difference_on_submit = reco.difference_amount
self.assert_reco_difference_matches_gl(reco.name)
# Landed cost retroactively raises the receipt (and batch) valuation, reposting the reco
make_landed_cost_voucher(
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=1000,
company="_Test Company",
)
reco.load_from_db()
self.assertNotEqual(reco.difference_amount, difference_on_submit)
self.assert_reco_difference_matches_gl(reco.name)
def test_difference_amount_synced_with_gl_after_repost_serialized(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
make_landed_cost_voucher,
)
item_code = self.make_item(
"Test Serial Item Reco Difference Sync",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TSIRDS.####",
},
).name
warehouse = "_Test Warehouse - _TC"
# Receive 5 serial nos @ 100 (value 500)
pr = make_purchase_receipt(
item_code=item_code,
warehouse=warehouse,
qty=5,
rate=100,
posting_date=add_days(nowdate(), -5),
)
serial_nos = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)
# Reconcile the serial nos to 5 @ 500 => difference 2500 - 500 = 2000
reco = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=5,
rate=500,
serial_no="\n".join(serial_nos),
use_serial_batch_fields=1,
posting_date=add_days(nowdate(), -2),
)
difference_on_submit = reco.difference_amount
self.assert_reco_difference_matches_gl(reco.name)
# Landed cost retroactively raises the receipt (and serial) valuation, reposting the reco
make_landed_cost_voucher(
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=1000,
company="_Test Company",
)
reco.load_from_db()
self.assertNotEqual(reco.difference_amount, difference_on_submit)
self.assert_reco_difference_matches_gl(reco.name)
def test_make_stock_zero_for_serial_batch_item(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

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