Compare commits

..

33 Commits

Author SHA1 Message Date
Frappe PR Bot
b30aa5334b chore(release): Bumped to Version 16.36.0
# [16.36.0](https://github.com/frappe/erpnext/compare/v16.35.0...v16.36.0) (2026-09-23)

### Bug Fixes

* **accounts:** clarify revaluation journal filters ([eebd0bc](eebd0bc533))
* **accounts:** keep price list within user permissions ([#59231](https://github.com/frappe/erpnext/issues/59231)) ([385343a](385343a389))
* apply journal entry reversal backport to version 16 controller ([0db0cc5](0db0cc55d4))
* **assets:** count complete opening depreciation periods accurately ([900eb1e](900eb1e466))
* **asset:** skip missing checkbox columns in asset type patch (backport [#58416](https://github.com/frappe/erpnext/issues/58416)) ([#58417](https://github.com/frappe/erpnext/issues/58417)) ([3c9d9d6](3c9d9d62aa))
* avoid pricing rule help template errors ([#59097](https://github.com/frappe/erpnext/issues/59097)) ([e3ef3be](e3ef3be490))
* clear stock value for zero-quantity balances ([#58899](https://github.com/frappe/erpnext/issues/58899)) ([007a1f5](007a1f5528))
* **crm:** keep opportunity type default untranslated ([#59277](https://github.com/frappe/erpnext/issues/59277)) ([8eb9063](8eb906366d))
* do not zero out backdated stock at a stock reco adjustment entry ([#59270](https://github.com/frappe/erpnext/issues/59270)) ([06a8faa](06a8faa4ed))
* **journal-entry:** carry reference fields into reverse journal entry ([06cc291](06cc2914ba))
* **manufacturing:** apply mrp manufacturing buffer as a duration ([#59007](https://github.com/frappe/erpnext/issues/59007)) ([fa732ca](fa732ca3f4))
* **manufacturing:** classify MRP rows by purchase item, not missing BOM ([#58510](https://github.com/frappe/erpnext/issues/58510)) ([79858d5](79858d5af6))
* **manufacturing:** finish a work order after partial material consumption ([#59312](https://github.com/frappe/erpnext/issues/59312)) ([5d495c2](5d495c2763))
* **manufacturing:** include the mrp bucket that ends on to_date ([#59143](https://github.com/frappe/erpnext/issues/59143)) ([131343d](131343de28))
* negative stock value for moving average item with mixed batchwise valuation ([#59099](https://github.com/frappe/erpnext/issues/59099)) ([262fdf3](262fdf3e69))
* **permissions:** grant select on link targets to roles with write access ([#59123](https://github.com/frappe/erpnext/issues/59123)) ([854b0e4](854b0e4734))
* **pos:** quote the reversed row's rate on a consolidated credit note (backport [#59320](https://github.com/frappe/erpnext/issues/59320)) ([#59323](https://github.com/frappe/erpnext/issues/59323)) ([242f421](242f4219cb))
* **pos:** reset stale selected mode of payment on new invoice (backport [#59308](https://github.com/frappe/erpnext/issues/59308)) ([#59355](https://github.com/frappe/erpnext/issues/59355)) ([1e16c82](1e16c827ef))
* **quality:** block Quality Inspection submit without a sample size (backport [#59330](https://github.com/frappe/erpnext/issues/59330)) ([#59331](https://github.com/frappe/erpnext/issues/59331)) ([1ead4ce](1ead4cebcf))
* refresh price list exchange rate when currency changes ([0fe41ce](0fe41ce92e))
* remove company link filter on asset field in Asset Depreciation Schedule ([a436707](a436707efc))
* render letter head jinja in statement of accounts pdf ([31111a4](31111a4a30))
* resolve conflicts ([98fa0d4](98fa0d4cb4))
* resolve conflicts ([7ff2a33](7ff2a33a5b))
* resolve conflicts ([6fab4e0](6fab4e0203))
* resolve MRP purchase supplier with item group fallback, else throw ([be283b7](be283b70f2))
* retain bom components without warehouse stock ([#59116](https://github.com/frappe/erpnext/issues/59116)) ([18ee3b9](18ee3b9cc0))
* **selling:** use preferred addresses in customer quotations ([c019bb5](c019bb5c28))
* skip serial nos with no stock when reconciling specific serial nos (backport [#59266](https://github.com/frappe/erpnext/issues/59266)) ([#59267](https://github.com/frappe/erpnext/issues/59267)) ([aaaba7c](aaaba7cd82))
* **stock:** allow zero completed quantity and handle process loss in job cards ([#59104](https://github.com/frappe/erpnext/issues/59104)) ([4f984ae](4f984aeecf))
* **stock:** apply transit warehouse filter to target warehouse in items table ([#59192](https://github.com/frappe/erpnext/issues/59192)) ([ae96dad](ae96dad151))
* **stock:** calculate batch bundle valuation per unit ([#58994](https://github.com/frappe/erpnext/issues/58994)) ([110192d](110192d9de))
* **stock:** compare transit quantities in stock UOM (backport [#59202](https://github.com/frappe/erpnext/issues/59202)) ([#59244](https://github.com/frappe/erpnext/issues/59244)) ([0f1cefa](0f1cefa918))
* **stock:** continue putaway allocation after undersized whole-uom rules ([#59155](https://github.com/frappe/erpnext/issues/59155)) ([dbb8981](dbb8981fa1))
* **stock:** deduct rejected qty from the in-transit warehouse ([#59299](https://github.com/frappe/erpnext/issues/59299)) ([4186d6c](4186d6c994))
* **stock:** filter item group defaults by company and non-group records ([#58923](https://github.com/frappe/erpnext/issues/58923)) ([741ae5d](741ae5d33c))
* **stock:** forward company to outward batch lookup ([#59160](https://github.com/frappe/erpnext/issues/59160)) ([5e1cfa4](5e1cfa4f52))
* **stock:** honour pick manually when mapping a pick list ([#59220](https://github.com/frappe/erpnext/issues/59220)) ([3ee9353](3ee9353240))
* **stock:** keep mapped cost center in stock entry items ([#59286](https://github.com/frappe/erpnext/issues/59286)) ([cdbdcc6](cdbdcc6e33))
* **stock:** respect manual picking when mapping sales orders ([58847de](58847de59a))
* **stock:** seed bin values when cancelling a stock voucher ([#59250](https://github.com/frappe/erpnext/issues/59250)) ([3ab13e1](3ab13e12d4)), closes [#57380](https://github.com/frappe/erpnext/issues/57380)
* **stock:** use net purchase values in landed cost vouchers (backport [#59274](https://github.com/frappe/erpnext/issues/59274)) ([#59278](https://github.com/frappe/erpnext/issues/59278)) ([6af5892](6af589208b))
* **stock:** use stored posting_datetime when reposting stock ledger entries ([#58998](https://github.com/frappe/erpnext/issues/58998)) ([39d6f2a](39d6f2a236))
* **stock:** validate warehouse account belongs to selected company ([#59191](https://github.com/frappe/erpnext/issues/59191)) ([38bdb4f](38bdb4f9c0))
* sync translations from crowdin (version-16-hotfix) ([#59210](https://github.com/frappe/erpnext/issues/59210)) ([d217283](d217283229))
* update modified for last purchase rate (backport [#59044](https://github.com/frappe/erpnext/issues/59044)) ([#59106](https://github.com/frappe/erpnext/issues/59106)) ([877747c](877747c133))
* use correct exchange rate for multi-currency bank reconciliation payments ([#59144](https://github.com/frappe/erpnext/issues/59144)) ([427a20c](427a20c508))
* validate bank reconciliation date ranges ([a878951](a878951c17))
* validate coupon pricing rules and validity dates ([2db2244](2db2244852))
* validate shipping rule account company ([590fff6](590fff6a45))
* valuation of serialized items after disabling serial no wise valuation ([#59197](https://github.com/frappe/erpnext/issues/59197)) ([af334da](af334dabcc))
* write off stranded stock value only when the warehouse is empty ([#59216](https://github.com/frappe/erpnext/issues/59216)) ([8bbb73d](8bbb73d2e2))

### Features

* mark update_serial_batch_entries patch as never skippable (backport [#59147](https://github.com/frappe/erpnext/issues/59147)) ([#59148](https://github.com/frappe/erpnext/issues/59148)) ([d2042bc](d2042bc1bb))
* **selling:** group Sales Order Analysis by item (backport [#59236](https://github.com/frappe/erpnext/issues/59236)) ([#59246](https://github.com/frappe/erpnext/issues/59246)) ([814d7f0](814d7f0c4e))
* **stock:** GL-only reposting from Stock and Account Value Comparison report ([#59127](https://github.com/frappe/erpnext/issues/59127)) ([c055faa](c055faabc1))
* use serial no wise valuation switch on item ([#59082](https://github.com/frappe/erpnext/issues/59082)) ([f09ce05](f09ce0583f))

### Performance Improvements

* filter by company in get_returned_invoice_items ([423aa09](423aa09f59))
* reduce memory consumption during reposting (backport [#59117](https://github.com/frappe/erpnext/issues/59117)) ([#59149](https://github.com/frappe/erpnext/issues/59149)) ([16b1821](16b1821f35)), closes [#52152](https://github.com/frappe/erpnext/issues/52152)
* skip legacy batch ledger lookups when no legacy entry exists (backport [#59110](https://github.com/frappe/erpnext/issues/59110)) ([#59111](https://github.com/frappe/erpnext/issues/59111)) ([4af1b0d](4af1b0db4e))
* **stock:** chunk the serial and batch entry backfill patch ([#59076](https://github.com/frappe/erpnext/issues/59076)) ([86fe0c1](86fe0c1f4b))
2026-09-23 23:05:32 +00:00
Diptanil Saha
eddb0c8aef Merge pull request #59272 from frappe/version-16-hotfix
chore: release v16
2026-09-24 04:33:27 +05:30
Frappe PR Bot
12cd563fb9 chore(release): Bumped to Version 16.35.0
# [16.35.0](https://github.com/frappe/erpnext/compare/v16.34.2...v16.35.0) (2026-09-15)

### Bug Fixes

* % Amount Billed exceeds 100% on fully rejected Purchase Receipts ([#58897](https://github.com/frappe/erpnext/issues/58897)) ([79c8e4d](79c8e4db7f))
* **accounts:** add permission checks on `execute_repost_payment_ledger` (backport [#58980](https://github.com/frappe/erpnext/issues/58980)) ([#58982](https://github.com/frappe/erpnext/issues/58982)) ([9bc98ef](9bc98efe2f))
* **accounts:** add permission checks on `invoice_discounting.get_invoices` (backport [#58975](https://github.com/frappe/erpnext/issues/58975)) ([#58990](https://github.com/frappe/erpnext/issues/58990)) ([a9f3d69](a9f3d69fbe))
* **accounts:** reject disabled price lists in buying and selling ([#58891](https://github.com/frappe/erpnext/issues/58891)) ([5bd623c](5bd623c4dd))
* **accounts:** stop counting received qty on non-stock invoice returns (backport [#58924](https://github.com/frappe/erpnext/issues/58924)) ([#58929](https://github.com/frappe/erpnext/issues/58929)) ([3856638](3856638d01))
* **asset:** recompute reference doc reqd on asset type change ([6d447f6](6d447f6847))
* **assets:** build Finance Books even when Calculate Depreciation is checked before Net Purchase Amount is entered ([1652f6e](1652f6ea42))
* **assets:** discard stale set_finance_book responses ([dfde7d8](dfde7d811c))
* avoid TypeError on new Work Order when company is not set ([#58969](https://github.com/frappe/erpnext/issues/58969)) ([e20501a](e20501a15c))
* calculate holiday list totals for half days ([#58792](https://github.com/frappe/erpnext/issues/58792)) ([3d6afff](3d6afffcef))
* coerce cost_allocation_per to float in BOM cost allocation ([1918ed6](1918ed6a3c))
* keep a dunning claimable until its interest is paid too ([7723f5a](7723f5aaed))
* keep each row's own rate under maintain same rate ([855d038](855d0385da))
* **manufacturing:** handle empty bom cost allocation ([#58939](https://github.com/frappe/erpnext/issues/58939)) ([77d8d5b](77d8d5b3e2))
* **manufacturing:** handle empty raw materials in workstation ([#58927](https://github.com/frappe/erpnext/issues/58927)) ([3b787c9](3b787c9443))
* **mrp:** align monthly bucket labels with demand dates ([#58963](https://github.com/frappe/erpnext/issues/58963)) ([c5773e7](c5773e7899))
* prevent disabled items from being used in bom ([#58997](https://github.com/frappe/erpnext/issues/58997)) ([f1a7fd0](f1a7fd006d))
* read posting date confirmation setting from boot data ([1349e0b](1349e0b3ab))
* refresh posting date confirmation setting ([a404d9b](a404d9b4e8))
* reset price list fallback when changing parties ([#58893](https://github.com/frappe/erpnext/issues/58893)) ([6751264](6751264ba5))
* resolve conflicts ([2add72b](2add72ba36))
* **selling:** add email permission check on `send_emails` (backport [#58935](https://github.com/frappe/erpnext/issues/58935)) ([#58938](https://github.com/frappe/erpnext/issues/58938)) ([a79abc0](a79abc0462))
* **selling:** exclude fully billed orders from the invoice picker (backport [#58966](https://github.com/frappe/erpnext/issues/58966)) ([#58967](https://github.com/frappe/erpnext/issues/58967)) ([5d9cbd9](5d9cbd9755))
* **selling:** list billable sales orders oldest first (backport [#59010](https://github.com/frappe/erpnext/issues/59010)) ([#59011](https://github.com/frappe/erpnext/issues/59011)) ([8672a6e](8672a6e604))
* **stock:** distribute additional costs when incoming items have no value ([b603581](b6035810f0))
* **stock:** do not rebuild the Warehouse tree on an account map miss (backport [#58956](https://github.com/frappe/erpnext/issues/58956)) ([#58973](https://github.com/frappe/erpnext/issues/58973)) ([6fb76eb](6fb76eba71))
* **stock:** guard inventory dimension lookup without reference document ([#58889](https://github.com/frappe/erpnext/issues/58889)) ([b21ca36](b21ca36428))
* **stock:** ignore disabled price lists on transactions (backport [#58926](https://github.com/frappe/erpnext/issues/58926)) ([#58931](https://github.com/frappe/erpnext/issues/58931)) ([53e5339](53e53390f4))
* **stock:** remove empty delivery stops after mapping ([#58896](https://github.com/frappe/erpnext/issues/58896)) ([ca33019](ca33019fdf))
* **stock:** set item_name to fetch if empty in packing slip item ([#58925](https://github.com/frappe/erpnext/issues/58925)) ([9e5d87a](9e5d87ac4a))
* **stock:** show actual outgoing rate in ledger preview ([#58700](https://github.com/frappe/erpnext/issues/58700)) ([2932793](2932793af0))
* **stock:** use correct field when reading previous stock closing balance (backport [#55898](https://github.com/frappe/erpnext/issues/55898)) ([#58854](https://github.com/frappe/erpnext/issues/58854)) ([70f391d](70f391d5e7)), closes [#54819](https://github.com/frappe/erpnext/issues/54819)
* **stock:** value stock entry rows as of the posting date (backport [#59042](https://github.com/frappe/erpnext/issues/59042)) ([#59048](https://github.com/frappe/erpnext/issues/59048)) ([7db6303](7db6303ac2))
* **subcontracting:** cannot select Against Finished Good on customer receipt (backport [#58949](https://github.com/frappe/erpnext/issues/58949)) ([#58951](https://github.com/frappe/erpnext/issues/58951)) ([df62766](df62766157))
* **subcontracting:** validate project across the subcontracting flow ([#58965](https://github.com/frappe/erpnext/issues/58965)) ([0095560](0095560dc7))
* sync translations from crowdin (version-16-hotfix) ([#59052](https://github.com/frappe/erpnext/issues/59052)) ([442fcd3](442fcd33e9))
* validate shelf life for automatically created expiring batches ([#58911](https://github.com/frappe/erpnext/issues/58911)) ([fa504b8](fa504b87db))

### Features

* **stock:** multi select item and warehouse filters in warehouse wise item balance ([bf33bc1](bf33bc17c0))
2026-09-15 18:46:11 +00:00
Diptanil Saha
6441ecd784 Merge pull request #59073 from frappe/version-16-hotfix
chore: release v16
2026-09-16 00:14:39 +05:30
Frappe PR Bot
4048fb70e1 chore(release): Bumped to Version 16.34.2
## [16.34.2](https://github.com/frappe/erpnext/compare/v16.34.1...v16.34.2) (2026-09-08)

### Bug Fixes

* **accounts:** enforce account field allow-list on financial report filters (backport [#58790](https://github.com/frappe/erpnext/issues/58790)) ([#58849](https://github.com/frappe/erpnext/issues/58849)) ([6e39b42](6e39b42137))
* **accounts:** reject same-account internal transfers (backport [#58529](https://github.com/frappe/erpnext/issues/58529)) ([#58877](https://github.com/frappe/erpnext/issues/58877)) ([6ec3035](6ec30350d2))
* **accounts:** round journal entry totals to field precision (backport [#58629](https://github.com/frappe/erpnext/issues/58629)) ([#58875](https://github.com/frappe/erpnext/issues/58875)) ([9fff01d](9fff01db32))
* add reconciliation after submit logic for bank transactions ([#57330](https://github.com/frappe/erpnext/issues/57330)) ([0a60d68](0a60d6805f))
* **bank reconciliation:** match Payment Entries on the bank-side amount (backport [#57740](https://github.com/frappe/erpnext/issues/57740)) ([#58765](https://github.com/frappe/erpnext/issues/58765)) ([189bd1f](189bd1f39d))
* **banking:** Federal bank dark logo (backport [#58844](https://github.com/frappe/erpnext/issues/58844)) ([#58845](https://github.com/frappe/erpnext/issues/58845)) ([3cc73e4](3cc73e4282))
* **banking:** find transfers on the same day (backport [#58766](https://github.com/frappe/erpnext/issues/58766)) ([#58767](https://github.com/frappe/erpnext/issues/58767)) ([8229aea](8229aeaead))
* **banking:** UI cleanup and better statement parsing (backport [#58817](https://github.com/frappe/erpnext/issues/58817)) ([#58824](https://github.com/frappe/erpnext/issues/58824)) ([0610708](0610708d78))
* **batch:** show Expired status only after expiry date has passed ([#58736](https://github.com/frappe/erpnext/issues/58736)) ([0684599](0684599bdb))
* check material request price list permission ([#58740](https://github.com/frappe/erpnext/issues/58740)) ([e5b1ff6](e5b1ff667d))
* check write permission in whitelisted document methods (backport [#58689](https://github.com/frappe/erpnext/issues/58689)) ([#58701](https://github.com/frappe/erpnext/issues/58701)) ([074f9f0](074f9f0828))
* **crm:** check read permission on lead in add_lead_to_prospect ([2cc1a51](2cc1a51d9a))
* fetch accounts by company (backport [#58523](https://github.com/frappe/erpnext/issues/58523)) ([#58879](https://github.com/frappe/erpnext/issues/58879)) ([75cb796](75cb796477))
* **gross-profit:** include item_name in export (backport [#58631](https://github.com/frappe/erpnext/issues/58631)) ([#58864](https://github.com/frappe/erpnext/issues/58864)) ([59b80b9](59b80b903a))
* **journal-entry:** avoid full grid re-render per row in set_exchange_rate (backport [#58328](https://github.com/frappe/erpnext/issues/58328)) ([#58803](https://github.com/frappe/erpnext/issues/58803)) ([8b51005](8b51005525))
* **manufacturing:** account for process loss in Production Plan Work Orders (backport [#58799](https://github.com/frappe/erpnext/issues/58799)) ([#58838](https://github.com/frappe/erpnext/issues/58838)) ([cfdf976](cfdf97601a))
* **manufacturing:** apply MOQ once across Production Plan rows (v16) ([#58831](https://github.com/frappe/erpnext/issues/58831)) ([f6dbb31](f6dbb3131d))
* **manufacturing:** apply safety stock once across Production Plan rows (v16) ([#58832](https://github.com/frappe/erpnext/issues/58832)) ([4719ad9](4719ad9b91))
* **manufacturing:** share transfer stock across Production Plan rows (v16) ([#58834](https://github.com/frappe/erpnext/issues/58834)) ([e59fb39](e59fb396e5))
* **manufacturing:** sum consolidated sub-assembly required quantity (v16) ([#58833](https://github.com/frappe/erpnext/issues/58833)) ([a7c5ab8](a7c5ab89e8))
* minor improvements to financial report template validation ([#58724](https://github.com/frappe/erpnext/issues/58724)) ([d23b407](d23b407ec7))
* order smallest purchase UOM qty that meets min order qty (backport [#57883](https://github.com/frappe/erpnext/issues/57883)) ([#58813](https://github.com/frappe/erpnext/issues/58813)) ([28f0ac5](28f0ac5287))
* persist rendered terms on transaction save ([#58858](https://github.com/frappe/erpnext/issues/58858)) ([89872eb](89872ebb37))
* **pos:** use company-currency change amount when netting pos gl entries (backport [#58599](https://github.com/frappe/erpnext/issues/58599)) ([#58678](https://github.com/frappe/erpnext/issues/58678)) ([06057cc](06057ccfc0))
* prevent duplicate Batch messages and Project links ([#58705](https://github.com/frappe/erpnext/issues/58705)) ([2dc2a04](2dc2a04522))
* resolve code lists by URI and version (backport [#58770](https://github.com/frappe/erpnext/issues/58770)) ([#58772](https://github.com/frappe/erpnext/issues/58772)) ([31319bd](31319bd36e))
* resolve conflicts ([d4b52c2](d4b52c236a))
* restore hover tooltip on Profit and Loss dashboard chart ([bb26f8f](bb26f8f7b9))
* **selling:** fetch orders within billing allowance (backport [#58751](https://github.com/frappe/erpnext/issues/58751)) ([#58820](https://github.com/frappe/erpnext/issues/58820)) ([817926c](817926ca2e))
* **setup:** strict permissions for transaction deletion record (backport [#58687](https://github.com/frappe/erpnext/issues/58687)) ([#58723](https://github.com/frappe/erpnext/issues/58723)) ([dfb64d7](dfb64d7635))
* show cost center for unmatched payments in receivables (backport [#58453](https://github.com/frappe/erpnext/issues/58453)) ([#58881](https://github.com/frappe/erpnext/issues/58881)) ([51bf7cb](51bf7cb241))
* **stock:** allow creating stock closing balances (backport [#58590](https://github.com/frappe/erpnext/issues/58590)) ([#58685](https://github.com/frappe/erpnext/issues/58685)) ([a1c8dc8](a1c8dc878d))
* **stock:** carry accounting dimensions from landed cost voucher charges into gl entries ([0c7be31](0c7be311b8))
* **stock:** recalculate delivery note billing after return ([#58869](https://github.com/frappe/erpnext/issues/58869)) ([7bb19c8](7bb19c89b1))
* **subcontracting:** add condition to exclude tax withholding accounts in purchase receipt mapping ([15a531f](15a531f03f))
* sync translations from crowdin (version-16-hotfix) ([#58580](https://github.com/frappe/erpnext/issues/58580)) ([6971c80](6971c80743))
* **timesheet:** handle empty allowed projects (backport [#58745](https://github.com/frappe/erpnext/issues/58745)) ([#58746](https://github.com/frappe/erpnext/issues/58746)) ([f75601e](f75601e9b1))
* use account currency in Bank Reconciliation Statement print (backport [#58692](https://github.com/frappe/erpnext/issues/58692)) ([#58871](https://github.com/frappe/erpnext/issues/58871)) ([b1bf95d](b1bf95d5b3))
* user not able to set valuation rate zero in stock reco (backport [#58800](https://github.com/frappe/erpnext/issues/58800)) ([#58823](https://github.com/frappe/erpnext/issues/58823)) ([fb13222](fb132225d7))

### Reverts

* **manufacturing:** remove material coverage changes on version 16 ([#58716](https://github.com/frappe/erpnext/issues/58716)) ([82392fa](82392fa747))
2026-09-08 13:23:31 +00:00
Diptanil Saha
cc57a7cf55 Merge pull request #58868 from frappe/version-16-hotfix
chore: release v16
2026-09-08 18:50:53 +05:30
Frappe PR Bot
0b50853985 chore(release): Bumped to Version 16.34.1
## [16.34.1](https://github.com/frappe/erpnext/compare/v16.34.0...v16.34.1) (2026-09-02)

### Bug Fixes

* filter cancelled BOMs in BOM Stock Analysis (backport [#58647](https://github.com/frappe/erpnext/issues/58647)) ([#58693](https://github.com/frappe/erpnext/issues/58693)) ([d6f9dde](d6f9dde3cb))
* improve message formatting and translation for validation issues ([#58425](https://github.com/frappe/erpnext/issues/58425)) ([a43de7c](a43de7ce95))
* include payment deductions in sales/purchase register ledger bal… (backport [#58437](https://github.com/frappe/erpnext/issues/58437)) ([#58680](https://github.com/frappe/erpnext/issues/58680)) ([4137401](41374019ba))
2026-09-02 12:24:04 +00:00
Sagar Vora
c19ddf187e Merge pull request #58707 from frappe/version-16-hotfix
chore: release v16
2026-09-02 17:51:22 +05:30
Frappe PR Bot
80c2c9e0e0 chore(release): Bumped to Version 16.34.0
# [16.34.0](https://github.com/frappe/erpnext/compare/v16.33.0...v16.34.0) (2026-09-02)

### Bug Fixes

* **accounts:** add permission checks on payment entry whitelisted methods ([#58644](https://github.com/frappe/erpnext/issues/58644)) ([c745558](c745558168))
* **accounts:** added permission checks on `get_available_payment_schedules` (backport [#58588](https://github.com/frappe/erpnext/issues/58588)) ([#58595](https://github.com/frappe/erpnext/issues/58595)) ([765a773](765a7736ed))
* **accounts:** guard get_gl_dict for v16 and drop unused base_gl_composer ([abf311e](abf311e8ca))
* **accounts:** prevent child table doctypes as accounting dimensions ([6dbaa50](6dbaa5048e))
* **accounts:** resolve subscription plans for any reference doctype in Payment Request (backport [#58438](https://github.com/frappe/erpnext/issues/58438)) ([#58450](https://github.com/frappe/erpnext/issues/58450)) ([3c63fff](3c63fffcb6))
* **accounts:** set pos profile on invoices respecting user permissions (backport [#58508](https://github.com/frappe/erpnext/issues/58508)) ([#58519](https://github.com/frappe/erpnext/issues/58519)) ([23290a6](23290a60da))
* add missing perm checks on various whitelisted methods (backport [#58652](https://github.com/frappe/erpnext/issues/58652)) ([#58654](https://github.com/frappe/erpnext/issues/58654)) ([86e9480](86e9480362))
* add type hints to make_request_for_quotation ([4435f16](4435f166ea))
* clarify duplicate internal party messages ([#58469](https://github.com/frappe/erpnext/issues/58469)) ([f0e603a](f0e603aec5))
* **company:** throw if linked to demo_company field ([9f3dd7e](9f3dd7eabd))
* correct is_frozen description on customer and supplier ([#58168](https://github.com/frappe/erpnext/issues/58168)) ([9e00888](9e00888d67))
* **crm:** add missing permission validation on `get_contract_template` (backport [#58621](https://github.com/frappe/erpnext/issues/58621)) ([#58623](https://github.com/frappe/erpnext/issues/58623)) ([30f0cd9](30f0cd9c95))
* **crm:** check write permission in edit_note ([c2261f6](c2261f6129))
* **crm:** validate contact email before saving an email campaign (backport [#58667](https://github.com/frappe/erpnext/issues/58667)) ([#58672](https://github.com/frappe/erpnext/issues/58672)) ([f22f84a](f22f84a58a))
* do not map the same row twice in "Get Items From" (backport [#58617](https://github.com/frappe/erpnext/issues/58617)) ([#58619](https://github.com/frappe/erpnext/issues/58619)) ([f3379b4](f3379b4694))
* explain missing fields when loading party details or taxes (backport [#58627](https://github.com/frappe/erpnext/issues/58627)) ([#58628](https://github.com/frappe/erpnext/issues/58628)) ([8a8bafd](8a8bafd6cb))
* filter fully ordered items when creating RFQ from Material Request ([9b1d907](9b1d907b6a))
* handle BOM price list currency update (backport [#58638](https://github.com/frappe/erpnext/issues/58638)) ([#58639](https://github.com/frappe/erpnext/issues/58639)) ([fb74a03](fb74a030de))
* **manufacturing:** account for pending job card qty ([#58466](https://github.com/frappe/erpnext/issues/58466)) ([7b9cdb4](7b9cdb4926))
* **manufacturing:** classify MRP items without a BOM as Purchase (backport [#58509](https://github.com/frappe/erpnext/issues/58509)) ([#58513](https://github.com/frappe/erpnext/issues/58513)) ([ee3b224](ee3b22403d))
* **manufacturing:** handle duplicate root BOM items (backport [#58614](https://github.com/frappe/erpnext/issues/58614)) ([#58616](https://github.com/frappe/erpnext/issues/58616)) ([22755c4](22755c4ad6))
* **manufacturing:** preserve job card qty in mr transfer (backport [#58482](https://github.com/frappe/erpnext/issues/58482)) ([#58499](https://github.com/frappe/erpnext/issues/58499)) ([76d0cb3](76d0cb382d))
* **manufacturing:** use item warehouses in production plan work orders ([#58663](https://github.com/frappe/erpnext/issues/58663)) ([ec392a7](ec392a7bcf))
* **manufacturing:** use packed row delivery date ([#58570](https://github.com/frappe/erpnext/issues/58570)) ([e64fddf](e64fddfad5))
* persist redistributed additional costs during stock entry repost (backport [#58433](https://github.com/frappe/erpnext/issues/58433)) ([#58533](https://github.com/frappe/erpnext/issues/58533)) ([629bc20](629bc20e90))
* **pos:** add permission checks on `get_invoices` (backport [#58591](https://github.com/frappe/erpnext/issues/58591)) ([#58597](https://github.com/frappe/erpnext/issues/58597)) ([d95eeb0](d95eeb0f6a))
* **projects:** check read permission on source in create_duplicate_project ([bd35957](bd35957582))
* **projects:** ignore cancelled invoices in timesheet portal (backport [#58501](https://github.com/frappe/erpnext/issues/58501)) ([#58504](https://github.com/frappe/erpnext/issues/58504)) ([d732d06](d732d06e25))
* round party balances to currency precision in Trial Balance for Party ([956a6ed](956a6ed407))
* **selling:** check quotation write permission before marking lost ([#58493](https://github.com/frappe/erpnext/issues/58493)) ([4e74831](4e74831a08))
* **selling:** check sales order permission before work order creation (backport [#58492](https://github.com/frappe/erpnext/issues/58492)) ([#58497](https://github.com/frappe/erpnext/issues/58497)) ([2b8e12e](2b8e12ef44))
* **sms_settings:** add patch to pre-fill roles into SMS Settings Roles Table ([4b89ddf](4b89ddffde))
* **stock:** auto-select batch no before saving transaction records (backport [#58536](https://github.com/frappe/erpnext/issues/58536)) ([#58539](https://github.com/frappe/erpnext/issues/58539)) ([eb4c327](eb4c327a2b))
* **stock:** ignore cancelled batch entries in valuation (backport [#58659](https://github.com/frappe/erpnext/issues/58659)) ([#58660](https://github.com/frappe/erpnext/issues/58660)) ([f4ed4ca](f4ed4cae64))
* **stock:** keep pick list links when refetching stock entry items ([01e0844](01e0844d40))
* **stock:** load available serial no report (backport [#58558](https://github.com/frappe/erpnext/issues/58558)) ([#58562](https://github.com/frappe/erpnext/issues/58562)) ([06758f2](06758f2792))
* **stock:** preserve exchange gain loss journals in lcv ([#58505](https://github.com/frappe/erpnext/issues/58505)) ([1242316](124231681a))
* **stock:** validate serial inventory dimensions ([#58394](https://github.com/frappe/erpnext/issues/58394)) ([a1aabaf](a1aabaff18))
* sum item and its alternate transferred qty on work order required items ([1411835](1411835507))
* sync translations from crowdin (version-16-hotfix) ([#58455](https://github.com/frappe/erpnext/issues/58455)) ([c924de3](c924de3c55))
* **taxes:** skip tax addition for invoice created from opening invoice tool ([bfbc13d](bfbc13d147))
* **timesheet:** scoping whitelisted methods output to projects and timesheets that are acccessible to users (backport [#58267](https://github.com/frappe/erpnext/issues/58267)) ([#58577](https://github.com/frappe/erpnext/issues/58577)) ([4f409e5](4f409e5ccf))
* translate doctype in map msg (backport [#58515](https://github.com/frappe/erpnext/issues/58515)) ([#58517](https://github.com/frappe/erpnext/issues/58517)) ([f0d2d40](f0d2d40d35))
* translate label in party validation (backport [#58624](https://github.com/frappe/erpnext/issues/58624)) ([#58626](https://github.com/frappe/erpnext/issues/58626)) ([9bb8d4f](9bb8d4ff93))
* validate items against source Sales Order in Material Request ([6546646](65466464df))
* validate POS Settings invoice and search fields on the server (backport [#58611](https://github.com/frappe/erpnext/issues/58611)) ([#58620](https://github.com/frappe/erpnext/issues/58620)) ([f8c2927](f8c29274a5))
* Work Order picks wrong Delivery Date when Sales Order has the same item in multiple rows ([#58526](https://github.com/frappe/erpnext/issues/58526)) ([cb4ec72](cb4ec72370))

### Features

* **analytics:** filter sales and purchase analytics by entity ([#58402](https://github.com/frappe/erpnext/issues/58402)) ([655ed81](655ed81575))
* **manufacturing:** add multi-currency support to Blanket Orders (backport [#58472](https://github.com/frappe/erpnext/issues/58472)) ([#58630](https://github.com/frappe/erpnext/issues/58630)) ([ff656b9](ff656b9ad2))
* valuation type for BOM secondary items (backport [#58431](https://github.com/frappe/erpnext/issues/58431)) ([#58632](https://github.com/frappe/erpnext/issues/58632)) ([1b44df4](1b44df480f))
2026-09-02 07:13:39 +00:00
Diptanil Saha
600cbf36af Merge pull request #58645 from frappe/version-16-hotfix
chore: release v16
2026-09-02 12:42:03 +05:30
Frappe PR Bot
b24c9eba55 chore(release): Bumped to Version 16.33.0
# [16.33.0](https://github.com/frappe/erpnext/compare/v16.32.3...v16.33.0) (2026-08-25)

### Bug Fixes

* **accounts:** allocate drop-ship cost by invoice quantity (backport [#58226](https://github.com/frappe/erpnext/issues/58226)) ([#58263](https://github.com/frappe/erpnext/issues/58263)) ([a62a949](a62a949b2a))
* **accounts:** disallow reversing a reverse journal entry ([8a26834](8a26834704))
* **accounts:** match returns to source invoice items (backport [#58250](https://github.com/frappe/erpnext/issues/58250)) ([#58264](https://github.com/frappe/erpnext/issues/58264)) ([9b0db26](9b0db26c25))
* **accounts:** supplier group filter not applied on accounts payable report ([d23b919](d23b9197d5))
* aggregate child warehouses in Stock Qty vs Serial No Count report (backport [#58134](https://github.com/frappe/erpnext/issues/58134)) ([#58366](https://github.com/frappe/erpnext/issues/58366)) ([abe4c0a](abe4c0a6b7))
* apply Sales Person user permissions in Accounts Receivable ([956be58](956be58c9c))
* **assets:** split FIFO/LIFO rate across grouped stock item rows ([98bef1c](98bef1cac8))
* **bank_statement_import:** add missing permission check on `get_import_status` (backport [#58217](https://github.com/frappe/erpnext/issues/58217)) ([#58219](https://github.com/frappe/erpnext/issues/58219)) ([2b685ed](2b685ed982))
* **bank_statement_import:** add missing permission check on multiple whitelisted methods (backport [#58221](https://github.com/frappe/erpnext/issues/58221)) ([#58224](https://github.com/frappe/erpnext/issues/58224)) ([81461ea](81461ea56c))
* block disabled/frozen party on Opportunity and Request for Quotation (backport [#57983](https://github.com/frappe/erpnext/issues/57983)) ([#58035](https://github.com/frappe/erpnext/issues/58035)) ([9f99ba6](9f99ba6ddc))
* **buying:** allow purchase returns against a closed purchase order ([#58139](https://github.com/frappe/erpnext/issues/58139)) ([61b5499](61b549960e))
* conflicts ([cbe7931](cbe7931b3b))
* correct Item Group doctype name in item tax template dashboard  (backport [#58192](https://github.com/frappe/erpnext/issues/58192)) ([#58213](https://github.com/frappe/erpnext/issues/58213)) ([56ac872](56ac87292f))
* correct to and from date filters in timesheet billing summary re… (backport [#58317](https://github.com/frappe/erpnext/issues/58317)) ([#58318](https://github.com/frappe/erpnext/issues/58318)) ([2077e84](2077e844d4))
* **crm_settings:** create custom fields for Frappe CRM on enabling synchronization ([795cf85](795cf8544c))
* describe stale exchange rate settings ([1ec2a2c](1ec2a2cd5d))
* don't set work order status to In Process only due to skip material transfer ([#58245](https://github.com/frappe/erpnext/issues/58245)) ([dd4d108](dd4d10862d))
* drop removed Restaurant doctype from sales tax template dashboard  (backport [#58191](https://github.com/frappe/erpnext/issues/58191)) ([#58211](https://github.com/frappe/erpnext/issues/58211)) ([c3cf7f2](c3cf7f2e91))
* **email_digest:** added permission check for `get_msg_html` (backport [#58197](https://github.com/frappe/erpnext/issues/58197)) ([#58200](https://github.com/frappe/erpnext/issues/58200)) ([32ebd69](32ebd69abc))
* escape interpolated values in portal, print and desk templates (backport [#58273](https://github.com/frappe/erpnext/issues/58273)) ([#58278](https://github.com/frappe/erpnext/issues/58278)) ([6b7b479](6b7b4796b4))
* escape interpolated values in text positions across portal and desk templates (backport [#58286](https://github.com/frappe/erpnext/issues/58286)) ([#58288](https://github.com/frappe/erpnext/issues/58288)) ([d357310](d3573105c4))
* escape on status image for workstations in production status (backport [#58279](https://github.com/frappe/erpnext/issues/58279)) ([#58281](https://github.com/frappe/erpnext/issues/58281)) ([9e2173e](9e2173e4c2))
* filter available batch report by company (backport [#57995](https://github.com/frappe/erpnext/issues/57995)) ([#58077](https://github.com/frappe/erpnext/issues/58077)) ([5933476](5933476778))
* get items from sales order in sales invoice ([#58163](https://github.com/frappe/erpnext/issues/58163)) ([0255314](0255314ea7))
* hide rfq status in supplier portal ([#58368](https://github.com/frappe/erpnext/issues/58368)) ([8a2109e](8a2109e62a))
* hide supplier name in rfq portal (backport [#58373](https://github.com/frappe/erpnext/issues/58373)) ([#58376](https://github.com/frappe/erpnext/issues/58376)) ([2329ef6](2329ef6424))
* ignore historical negative batch stock in outward validation (backport [#58148](https://github.com/frappe/erpnext/issues/58148)) ([#58151](https://github.com/frappe/erpnext/issues/58151)) ([c18881b](c18881b37d))
* Ignore permission while deleting user permission (backport [#57296](https://github.com/frappe/erpnext/issues/57296)) ([#57760](https://github.com/frappe/erpnext/issues/57760)) ([98d69b2](98d69b2ae3))
* include time logs ending at midnight in timesheet billing summary (backport [#58355](https://github.com/frappe/erpnext/issues/58355)) ([#58357](https://github.com/frappe/erpnext/issues/58357)) ([0bf34c0](0bf34c054a))
* **italy:** handle none price_list_rate in e-invoice xml generation (backport [#58242](https://github.com/frappe/erpnext/issues/58242)) ([#58370](https://github.com/frappe/erpnext/issues/58370)) ([feb51a4](feb51a475a))
* keep Currency and Price List section open for foreign currency (backport [#58074](https://github.com/frappe/erpnext/issues/58074)) ([#58113](https://github.com/frappe/erpnext/issues/58113)) ([a3a6efb](a3a6efbcea))
* **manufacturing:** derive material transfers from actual coverage ([#58115](https://github.com/frappe/erpnext/issues/58115)) ([2a9e430](2a9e4304a2))
* **manufacturing:** fall back to item group defaults for work order w… ([#58237](https://github.com/frappe/erpnext/issues/58237)) ([861fb26](861fb26b2c))
* **manufacturing:** keep MRP chart dates distinct ([1b37fd2](1b37fd2edc))
* **Material Requirements Planning Report:** detailed-view chart timescale ([de06cb7](de06cb7f45)), closes [#52632](https://github.com/frappe/erpnext/issues/52632)
* new docs should refetch incoming rates (backport [#58097](https://github.com/frappe/erpnext/issues/58097)) ([#58295](https://github.com/frappe/erpnext/issues/58295)) ([edb9b33](edb9b33264))
* patch to delete the `crm_deal` custom fields ([d66dc14](d66dc143e3))
* prevent duplicate supplier quotations from portal ([39e15c7](39e15c7b2d))
* prevent TimestampMismatchError resolving Dunning with multiple overdue installments ([5764d1d](5764d1d173))
* **projects:** respect permissions in timesheet billing summary (backport [#58320](https://github.com/frappe/erpnext/issues/58320)) ([#58322](https://github.com/frappe/erpnext/issues/58322)) ([647c933](647c93321b))
* Qty and UOM not fetched when adding Item in Material Request ([#58118](https://github.com/frappe/erpnext/issues/58118)) ([aecc551](aecc551b5e))
* renaming the overdue billing checkbox (backport [#58165](https://github.com/frappe/erpnext/issues/58165)) ([#58166](https://github.com/frappe/erpnext/issues/58166)) ([1ea1d5d](1ea1d5d6a5))
* render missing terms before printing ([#58358](https://github.com/frappe/erpnext/issues/58358)) ([f56cdac](f56cdac45f))
* resolve backport conflicts ([3fd3f94](3fd3f9485b))
* respect zero currency precision ([#58395](https://github.com/frappe/erpnext/issues/58395)) ([6b61146](6b61146d2f))
* rewriting the description of the overdue billing checkbox (backport [#58172](https://github.com/frappe/erpnext/issues/58172)) ([#58174](https://github.com/frappe/erpnext/issues/58174)) ([4bb7799](4bb7799919))
* run set_missing_values before creating Purchase Order from MRP report ([72114fa](72114fa738))
* **selling:** read overdue amount from payment ledger, not gl tags  (backport [#57786](https://github.com/frappe/erpnext/issues/57786)) ([#58026](https://github.com/frappe/erpnext/issues/58026)) ([29f6a5b](29f6a5b9cf))
* send auto reorder email to all managers in single company setup ([ac1c692](ac1c6921da))
* skip covered rows when ordering from the mrp report ([#58249](https://github.com/frappe/erpnext/issues/58249)) ([#58312](https://github.com/frappe/erpnext/issues/58312)) ([dfc2301](dfc2301f08))
* **stock:** confirm before changing item qty from the batch selector (backport [#58123](https://github.com/frappe/erpnext/issues/58123)) ([#58125](https://github.com/frappe/erpnext/issues/58125)) ([4f3ffd3](4f3ffd3820))
* **stock:** fetch item stock UOM in stock reconciliation (backport [#58284](https://github.com/frappe/erpnext/issues/58284)) ([#58291](https://github.com/frappe/erpnext/issues/58291)) ([1f21c0b](1f21c0b0af))
* **stock:** honour pick serial / batch based on in the batch selector (backport [#58176](https://github.com/frappe/erpnext/issues/58176)) ([#58182](https://github.com/frappe/erpnext/issues/58182)) ([d3a4d47](d3a4d476eb))
* **stock:** preserve item UOM conversion factor (backport [#58299](https://github.com/frappe/erpnext/issues/58299)) ([#58304](https://github.com/frappe/erpnext/issues/58304)) ([53b61e5](53b61e5ac6))
* **stock:** reset bin when no stock ledger entries remain ([#58362](https://github.com/frappe/erpnext/issues/58362)) ([37a1fd1](37a1fd11e9))
* stop doubling totals in timesheet billing summary (backport [#58311](https://github.com/frappe/erpnext/issues/58311)) ([#58315](https://github.com/frappe/erpnext/issues/58315)) ([c3313f5](c3313f5b34))
* use user data fields hook (backport [#58274](https://github.com/frappe/erpnext/issues/58274)) ([#58283](https://github.com/frappe/erpnext/issues/58283)) ([94b6622](94b6622d9d))
* validation for task end date check ([7b0df4b](7b0df4b28b))
* work order finish dialog with process loss qty from job card (backport [#58256](https://github.com/frappe/erpnext/issues/58256)) ([#58262](https://github.com/frappe/erpnext/issues/58262)) ([27a04d8](27a04d8e08))

### Features

* **accounts:** opt-in 'Consider Accounting Dimension' filter on General Ledger Report (backport [#58156](https://github.com/frappe/erpnext/issues/58156)) ([#58158](https://github.com/frappe/erpnext/issues/58158)) ([f8c3270](f8c3270049))
* add status filter to Supplier Quotation Comparison report ([2b84ed7](2b84ed78e8))
* Belgian Charts of Accounts (commercial + non-profit, FR + NL) (backport [#54679](https://github.com/frappe/erpnext/issues/54679)) ([#58186](https://github.com/frappe/erpnext/issues/58186)) ([d805f4d](d805f4d3fc))
* taxable-base resolver hook for custom charge types ([#56175](https://github.com/frappe/erpnext/issues/56175)) ([ea238a9](ea238a90ac))
* validate purchase receipt exchange rate parity on purchase invoice (backport [#58177](https://github.com/frappe/erpnext/issues/58177)) ([#58190](https://github.com/frappe/erpnext/issues/58190)) ([900064e](900064e395))
2026-08-25 17:04:28 +00:00
Diptanil Saha
1a745cd433 Merge pull request #58397 from frappe/version-16-hotfix
chore: release v16
2026-08-25 22:21:37 +05:30
Frappe PR Bot
11e0ba0a1c chore(release): Bumped to Version 16.32.3
## [16.32.3](https://github.com/frappe/erpnext/compare/v16.32.2...v16.32.3) (2026-08-18)

### Bug Fixes

* Qty and UOM not fetched when adding Item in Material Request (backport [#58118](https://github.com/frappe/erpnext/issues/58118)) ([#58266](https://github.com/frappe/erpnext/issues/58266)) ([f01f741](f01f7415e1))
2026-08-18 09:57:04 +00:00
mergify[bot]
f01f7415e1 fix: Qty and UOM not fetched when adding Item in Material Request (backport #58118) (#58266) 2026-08-18 15:25:21 +05:30
Frappe PR Bot
5412ea0b8c chore(release): Bumped to Version 16.32.2
## [16.32.2](https://github.com/frappe/erpnext/compare/v16.32.1...v16.32.2) (2026-08-18)

### Bug Fixes

* get items from sales order in sales invoice (backport [#58163](https://github.com/frappe/erpnext/issues/58163)) ([#58258](https://github.com/frappe/erpnext/issues/58258)) ([204f979](204f979f33))
2026-08-18 07:29:42 +00:00
mergify[bot]
204f979f33 fix: get items from sales order in sales invoice (backport #58163) (#58258)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-18 07:27:23 +00:00
Frappe PR Bot
21d1873020 chore(release): Bumped to Version 16.32.1
## [16.32.1](https://github.com/frappe/erpnext/compare/v16.32.0...v16.32.1) (2026-08-14)

### Bug Fixes

* ignore historical negative batch stock in outward validation (backport [#58148](https://github.com/frappe/erpnext/issues/58148)) (backport [#58151](https://github.com/frappe/erpnext/issues/58151)) ([#58153](https://github.com/frappe/erpnext/issues/58153)) ([e79b4ab](e79b4abe9d))
2026-08-14 06:20:44 +00:00
mergify[bot]
e79b4abe9d fix: ignore historical negative batch stock in outward validation (backport #58148) (backport #58151) (#58153)
fix: ignore historical negative batch stock in outward validation (backport #58148) (#58151)

fix: ignore historical negative batch stock in outward validation (#58148)

(cherry picked from commit 9239d1c2a3)


(cherry picked from commit c18881b37d)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-08-14 11:48:55 +05:30
Frappe PR Bot
81a6f97566 chore(release): Bumped to Version 16.32.0
# [16.32.0](https://github.com/frappe/erpnext/compare/v16.31.1...v16.32.0) (2026-08-11)

### Bug Fixes

* add raw material to its operation even when another operation uses the item ([35f9bec](35f9bec988))
* add type hints to conversion factor API ([dbfe7e1](dbfe7e199e))
* allow non-admin roles to import chart of accounts (backport [#57454](https://github.com/frappe/erpnext/issues/57454)) ([#58060](https://github.com/frappe/erpnext/issues/58060)) ([a707c82](a707c82c0c))
* allow selecting a warehouse for new items in the update items dialog ([#57876](https://github.com/frappe/erpnext/issues/57876)) ([c9977be](c9977be5d4))
* cap a manufacture entry at the job card's pending production ([79ad410](79ad410cc1))
* clear deferred revenue/expense fields on uncheck (backport [#57140](https://github.com/frappe/erpnext/issues/57140)) ([7e72e70](7e72e70cc1))
* **coa_importer:** added server-side validations for importing chart of accounts (backport [#58065](https://github.com/frappe/erpnext/issues/58065)) ([#58066](https://github.com/frappe/erpnext/issues/58066)) ([db22fcb](db22fcb157))
* condition check with empty object for falsy case ([838fb8e](838fb8e8df))
* **consolidated cash flow:** correct totals and labels in section foo… (backport [#57336](https://github.com/frappe/erpnext/issues/57336)) ([#58056](https://github.com/frappe/erpnext/issues/58056)) ([f11d0d5](f11d0d5cec))
* **controllers:** correct negative rate settings link ([51aecec](51aecec598))
* convert hours to minutes in workstation complete_job ([822e6d8](822e6d8924))
* declare precision 9 on all conversion_factor fields ([db49b03](db49b03913))
* derive operation FG items before material expansion, keep the final one the BOM's item ([3e0d0b2](3e0d0b2d68))
* don't demand raw material transfer for semi FG job cards on submit ([99d9d84](99d9d845bd))
* escape `customer_details` on lead creation from appointment (backport [#57947](https://github.com/frappe/erpnext/issues/57947)) ([#57949](https://github.com/frappe/erpnext/issues/57949)) ([dff4ec6](dff4ec6a74))
* field validation and perm checks on `get_stock_reservation_entries_for_voucher` (backport [#57968](https://github.com/frappe/erpnext/issues/57968)) ([#57970](https://github.com/frappe/erpnext/issues/57970)) ([0a9b632](0a9b632925))
* generate the next manufacture entry net of booked process loss ([2548751](2548751673))
* guard reconciliation table deletes when tables are missing ([21fbfa5](21fbfa541d))
* handling negative grand total ([e657a7f](e657a7f19f))
* incorrect batch-wise valuation rate for entries with same posting datetime (backport [#57794](https://github.com/frappe/erpnext/issues/57794)) ([#57797](https://github.com/frappe/erpnext/issues/57797)) ([adfa676](adfa6768c9))
* incorrect entry detection in Stock Ledger Invariant Check ([#57886](https://github.com/frappe/erpnext/issues/57886)) ([f7bae88](f7bae888cf))
* item property updates in POS and transactions and add styling (backport [#57189](https://github.com/frappe/erpnext/issues/57189)) ([#58058](https://github.com/frappe/erpnext/issues/58058)) ([5f43c89](5f43c89c45))
* **job_card:** apply the completion dialog's qty to manufacture ([#57685](https://github.com/frappe/erpnext/issues/57685)) ([d97cf13](d97cf131a1))
* **job_card:** leave the pending qty out of the job card's own output ([#57686](https://github.com/frappe/erpnext/issues/57686)) ([0c79194](0c7919429e))
* **job_card:** reject a completion split that cannot add up ([#57687](https://github.com/frappe/erpnext/issues/57687)) ([3907d93](3907d93f9f))
* **job_card:** require the previous operation to be manufactured ([#57684](https://github.com/frappe/erpnext/issues/57684)) ([a22a7fd](a22a7fddba))
* **journal_entry:** validate blocked purchase invoices ([04718e1](04718e15c9))
* **journal_entry:** validation message for blocked purchase invoice ([#57896](https://github.com/frappe/erpnext/issues/57896)) ([f0adbd2](f0adbd2bd3))
* keep asset repair downtime in sync with entered dates ([ca5418e](ca5418e2c9))
* keep job card actions visible for corrective job cards ([4ee276d](4ee276d3db))
* keep source rate on re-fetch when maintain same rate is enabled (backport [#57479](https://github.com/frappe/erpnext/issues/57479)) ([#57792](https://github.com/frappe/erpnext/issues/57792)) ([ee5316e](ee5316ecd0))
* keep Target Warehouse optional for work orders tracking semi finished goods ([c598cf9](c598cf9010))
* keep the transfer qty check for legacy semi FG cards without an FG item ([f810d78](f810d780c0))
* **manufacturing:** adapt version 16 compatibility ([a8060d5](a8060d5e99))
* **manufacturing:** align quantity split rounding ([317dd18](317dd18ce5))
* **manufacturing:** avoid child BOM cache lookups ([fb87751](fb877515ca))
* **manufacturing:** correct nested BOM quantities ([b4929e2](b4929e2737))
* **manufacturing:** keep item code searchable when a barcode matches the same text ([87c4009](87c4009572))
* **manufacturing:** type whitelisted BOM arguments ([87b456f](87b456faa2))
* mirror rounding adjustment on distributed_discount_amount (backport [#58047](https://github.com/frappe/erpnext/issues/58047)) ([#58055](https://github.com/frappe/erpnext/issues/58055)) ([d680115](d680115fcb))
* optimize product bundle item search ([2934971](29349711e4))
* preserve custom title on new JV (backport [#57987](https://github.com/frappe/erpnext/issues/57987)) ([#57989](https://github.com/frappe/erpnext/issues/57989)) ([4a2163e](4a2163ebf8))
* preserve original operation idx in manually created Job Cards ([fee003a](fee003a82d))
* purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (version-16-hotfix) ([#57836](https://github.com/frappe/erpnext/issues/57836)) ([aa70d9b](aa70d9bbc3))
* re-check future sle before queuing repost on submit ([#57664](https://github.com/frappe/erpnext/issues/57664)) ([#57961](https://github.com/frappe/erpnext/issues/57961)) ([2b020c2](2b020c2fcf))
* reflect in-invoice receivable settlements in Sales Register ledger view ([a90907f](a90907fda6))
* **regional:** rename Italy's duplicate Customer name fields ([f56867d](f56867d843)), closes [#46281](https://github.com/frappe/erpnext/issues/46281) [#50915](https://github.com/frappe/erpnext/issues/50915) [#57215](https://github.com/frappe/erpnext/issues/57215) [#50921](https://github.com/frappe/erpnext/issues/50921) [#53409](https://github.com/frappe/erpnext/issues/53409)
* repost read stale sibling SLE rate for moving average returns ([ffa65b0](ffa65b0c48))
* require FG / Semi FG Item on operations when tracking semi finished goods ([03eeb83](03eeb839fb))
* require material transfer before job card start and completion ([a7f75de](a7f75de1aa))
* require WIP warehouse for work orders tracking semi finished goods ([24cd5f2](24cd5f22b5))
* roll up process loss to the work order for semi finished goods ([27130d8](27130d8e49))
* round computed conversion factors to field precision ([206ed28](206ed28924))
* round production plan mr_items quantity to field precision ([2d056ae](2d056aee3d))
* **sales-invoice:** respect Customize Form hidden setting on Update Stock ([#57819](https://github.com/frappe/erpnext/issues/57819)) ([0e26f9b](0e26f9b1db))
* scale generated raw materials to the manufacture entry's production share ([acba994](acba9945b0))
* scope manufacture entry process loss to its own job card ([a0b370b](a0b370b2e9)), closes [frappe/erpnext#57892](https://github.com/frappe/erpnext/issues/57892)
* **selling:** bill re-delivered sales order quantities ([6fb09b1](6fb09b1e21))
* **selling:** preserve explicit UOM during item selection ([218397e](218397e78d))
* **selling:** read overdue amount from payment ledger, not gl tags (backport [#57786](https://github.com/frappe/erpnext/issues/57786)) ([#58057](https://github.com/frappe/erpnext/issues/58057)) ([b89229a](b89229a93d))
* **selling:** reset stale item details on item change ([b5d727f](b5d727fb81))
* set `restrict_globals=True` in `frappe.render_template` (backport [#57899](https://github.com/frappe/erpnext/issues/57899)) ([#57902](https://github.com/frappe/erpnext/issues/57902)) ([6b45002](6b45002abc))
* **setup:** fetch driver address by supplier link ([62a851b](62a851bebb))
* skip incoming rate calc when serial no qty is zero (backport [#57427](https://github.com/frappe/erpnext/issues/57427)) ([#57957](https://github.com/frappe/erpnext/issues/57957)) ([a4d9411](a4d9411345))
* skip PO items with invoice-created receipts in billing repair patch ([9139994](9139994686))
* skip the pending production check on update-after-submit saves ([491f9fa](491f9fa3fe))
* **stock:** drop call to confirm_if_drafts_exist missing on v16 ([#57833](https://github.com/frappe/erpnext/issues/57833)) ([626e351](626e35135f))
* **stock:** handle multi-item opening balance in Stock Ledger report (backport [#57591](https://github.com/frappe/erpnext/issues/57591)) ([#57796](https://github.com/frappe/erpnext/issues/57796)) ([af3184c](af3184c8b4))
* **stock:** repair duplicated purchase receipt billing ([fa733e6](fa733e691b))
* **stock:** validate new warehouse inventory account after naming ([fce0eb1](fce0eb1577))
* **stock:** validate warehouse accounts when used ([0dfa54f](0dfa54f812))
* stop asking for a manufacturing entry when process loss explains the shortfall ([104c8df](104c8df765))
* **subscription:** don't reactivate a cancelled subscription (backport [#57774](https://github.com/frappe/erpnext/issues/57774))  ([eeb3cd2](eeb3cd238e)), closes [#57761](https://github.com/frappe/erpnext/issues/57761) [#56796](https://github.com/frappe/erpnext/issues/56796) [#56796](https://github.com/frappe/erpnext/issues/56796) [#57780](https://github.com/frappe/erpnext/issues/57780)
* sync open reference forms after Quality Inspection updates them ([33eb619](33eb6199d9))
* sync translations from crowdin ([#57841](https://github.com/frappe/erpnext/issues/57841)) ([a9d139a](a9d139ae6b))
* sync translations from crowdin ([#58069](https://github.com/frappe/erpnext/issues/58069)) ([f697b02](f697b024db))
* tolerate floating-point drift in sales team allocated percentage ([39f15bb](39f15bb3e9))
* use stock settings for warehouse defaults ([d8bbe86](d8bbe865a8))
* validate webform for project ([8d98fe8](8d98fe8187))
* zero-rate repost fallback could still read sibling SLE ([81f81ff](81f81fff32))

### Features

* **job_card:** print quantities with their stock uom ([#57689](https://github.com/frappe/erpnext/issues/57689)) ([eb90186](eb90186d3f))
* stock validations in Period Closing Voucher and snapshot-seeded batch valuation (backport [#57811](https://github.com/frappe/erpnext/issues/57811)) ([#57816](https://github.com/frappe/erpnext/issues/57816)) ([243266f](243266f5ef))
* sync serial no status from stock ledger in Stock Qty vs Serial No Count report (version-16-hotfix) ([#57864](https://github.com/frappe/erpnext/issues/57864)) ([ea5cbb1](ea5cbb116c))
2026-08-11 23:39:40 +00:00
Diptanil Saha
b27bc42d51 Merge pull request #58024 from frappe/version-16-hotfix
chore: release v16
2026-08-12 05:08:00 +05:30
Frappe PR Bot
22247ab7c5 chore(release): Bumped to Version 16.31.1
## [16.31.1](https://github.com/frappe/erpnext/compare/v16.31.0...v16.31.1) (2026-08-06)

### Bug Fixes

* **stock:** drop call to confirm_if_drafts_exist missing on v16 ([#57833](https://github.com/frappe/erpnext/issues/57833)) ([e834910](e834910bfd))
2026-08-06 08:12:42 +00:00
Mihir Kandoi
f8b19f7376 Merge pull request #57834 from frappe/mergify/bp/version-16/pr-57833
fix(stock): drop call to confirm_if_drafts_exist missing on v16 (backport #57833)
2026-08-06 13:40:55 +05:30
Mihir Kandoi
e834910bfd fix(stock): drop call to confirm_if_drafts_exist missing on v16 (#57833)
(cherry picked from commit 626e35135f)
2026-08-06 08:09:41 +00:00
Frappe PR Bot
68ea583a1f chore(release): Bumped to Version 16.31.0
# [16.31.0](https://github.com/frappe/erpnext/compare/v16.30.0...v16.31.0) (2026-08-04)

### Bug Fixes

* **accounts receivable:** made territory field multi select (backport [#57322](https://github.com/frappe/erpnext/issues/57322)) ([#57346](https://github.com/frappe/erpnext/issues/57346)) ([9b452f1](9b452f12b7))
* **accounts:** fetch deferred invoice docs on non-empty `sales_docs` or `purchase_docs` in repost accounting ledger ([#57753](https://github.com/frappe/erpnext/issues/57753)) ([6153202](6153202231))
* **accounts:** skip party dashboard without invoice permission ([ee6955d](ee6955d56c))
* **accounts:** update AU standard chart of accounts (backport [#57145](https://github.com/frappe/erpnext/issues/57145)) ([#57608](https://github.com/frappe/erpnext/issues/57608)) ([1ea1ce1](1ea1ce15d0))
* allow custom remark on reversal journal entry ([b4dfca9](b4dfca9ef1))
* apply the supplier selection to this branch's own mapper module ([e5f8d0c](e5f8d0c84b))
* **assets:** add permission checks on whitelisted methods on `asset_capitalization` ([99f249b](99f249b1a0))
* **banking:** fetch company list from DB instead of boot (backport [#57731](https://github.com/frappe/erpnext/issues/57731)) ([#57739](https://github.com/frappe/erpnext/issues/57739)) ([ca60653](ca6065398c))
* do not fetch a random inventory account when multiple inventory accounts exist (backport [#57626](https://github.com/frappe/erpnext/issues/57626)) ([#57632](https://github.com/frappe/erpnext/issues/57632)) ([34cbd3c](34cbd3c8d5))
* drop row prefix in reserve stock message when row is unknown ([684ae4d](684ae4d762))
* escape data in multiple templates (backport [#57742](https://github.com/frappe/erpnext/issues/57742)) ([#57770](https://github.com/frappe/erpnext/issues/57770)) ([824ae57](824ae57e44))
* escape item code and UOM in the supplier dialog errors ([06a753f](06a753faf3))
* exclude transferred and consumed qty from dashboard reserved stock ([46317b0](46317b063a))
* filter Accounts Receivable by invoice sales partner (backport [#57628](https://github.com/frappe/erpnext/issues/57628)) ([#57648](https://github.com/frappe/erpnext/issues/57648)) ([0e46937](0e46937f60))
* guard against None row in get_stock_balance_for (backport [#57567](https://github.com/frappe/erpnext/issues/57567)) ([283ee6e](283ee6e07b))
* guard scio row lookup in stock entry items_add ([fb6c87d](fb6c87dd0c))
* **italy:** skip e-invoicing for opening invoices ([5e584d1](5e584d1cfb))
* **item_variant:** added permission checks on `enqueue_multiple_variant_creation` ([c7cf9d8](c7cf9d868b))
* keep the bulk supplier field to half the supplier selection dialog ([2c9db13](2c9db13041))
* label the items table in the supplier selection dialog ([187840b](187840b559))
* let Purchase Receipt cancel defer to Frappe's linked-document check (backport [#57592](https://github.com/frappe/erpnext/issues/57592)) ([#57597](https://github.com/frappe/erpnext/issues/57597)) ([16be0f0](16be0f0944))
* link the item and spell out the unit in the supplier dialog errors ([810b9ae](810b9ae28f))
* list the Purchase Orders created per supplier instead of opening one ([d2fe4b6](d2fe4b623c))
* **manufacturing:** reach the whole configurator from tree toolbar actions ([281e92f](281e92fb6e)), closes [#57773](https://github.com/frappe/erpnext/issues/57773)
* open the Purchase Order when the supplier selection creates only one ([e71cef0](e71cef02b3))
* **opportunity:** add validation for positive item quantities ([a9f969e](a9f969e942))
* **payment reconciliation:** correct supplier gain/loss posting ([dc907ad](dc907add40))
* **payment_request:** added permission checks on `resend_payment_email` ([fcbbb25](fcbbb251cf))
* **plant_floor:** add missing perm check on `get_stock_summary` (backport [#57667](https://github.com/frappe/erpnext/issues/57667)) ([#57670](https://github.com/frappe/erpnext/issues/57670)) ([7bc0475](7bc04752fc))
* **pos:** don't double-escape Item Group names in get_item_groups ([#57673](https://github.com/frappe/erpnext/issues/57673)) ([a5544d0](a5544d0bfb))
* preserve UOM conversion factor precision in transactions ([e5999b2](e5999b22c7))
* prevent duplicate shipping charges without cost center ([666b616](666b6167a1))
* **projects:** add project filter ([3ba8313](3ba83134af))
* **projects:** include on hold status in project filters and reports ([d104d8e](d104d8e723))
* **purchase:** reject purchase returns where every item has zero quantity ([032b922](032b922f0c))
* **quotation:** carry forward communications from opportunity at after_insert (backport [#57639](https://github.com/frappe/erpnext/issues/57639)) ([#57643](https://github.com/frappe/erpnext/issues/57643)) ([624a236](624a236f88))
* reject the same Material Request item twice in one supplier selection ([ea770f6](ea770f6a8e))
* resolve version 16 backport conflicts ([e98471d](e98471d9c9))
* resolve version-16 backport conflicts ([697f68d](697f68d1d2))
* respect child warehouse account override in Stock and Account Value Comparison ([#57552](https://github.com/frappe/erpnext/issues/57552)) ([ca657d2](ca657d2629))
* respect quantity precision in material transfer validation ([eb969a5](eb969a5866))
* **sales:** reject sales returns where every item has zero quantity ([aa71cd6](aa71cd695b))
* seed standard Item Groups under the existing tree root ([4ffa950](4ffa950aa3)), closes [#57581](https://github.com/frappe/erpnext/issues/57581)
* **selling:** don't require cancel and delete perms to remove items via Update Items (backport [#57419](https://github.com/frappe/erpnext/issues/57419)) ([#57601](https://github.com/frappe/erpnext/issues/57601)) ([04e1ca8](04e1ca8226))
* set Required By on Purchase Orders created per supplier ([f99d66d](f99d66d578))
* set reservation voucher_qty to voucher demand not reserved qty ([ecccedf](ecccedf0ed))
* source manually created asset value from valuation rate ([b556b01](b556b012f0))
* **stock:** accept every number a reading can be written as ([fccf122](fccf1220f6))
* **stock:** correct secondary item valuation across stock entry purposes ([4ed0374](4ed03748fe)), closes [#57732](https://github.com/frappe/erpnext/issues/57732) [#57735](https://github.com/frappe/erpnext/issues/57735) [#57736](https://github.com/frappe/erpnext/issues/57736) [#57737](https://github.com/frappe/erpnext/issues/57737) [#57738](https://github.com/frappe/erpnext/issues/57738)
* **stock:** keep manufactured item rate at zero when inputs are free  (backport [#57334](https://github.com/frappe/erpnext/issues/57334)) ([#57513](https://github.com/frappe/erpnext/issues/57513)) ([49924dd](49924ddd1d))
* **stock:** read quality inspection readings in the user's number format ([3b7fb68](3b7fb6851a))
* **stock:** resolve backport conflict in quality inspection imports ([656db1c](656db1c2fe))
* **stock:** scope over deliver/receive role check to delivery and receipt overflow ([4713ddd](4713ddd55b))
* **stock:** validate only the variant attributes that changed ([0013908](00139081f6))
* **stock:** validate over delivery/receipt allowance in stock settings ([3f3292c](3f3292ca4a))
* **stock:** value batched packed-item returns from the original bundle  (backport [#57327](https://github.com/frappe/erpnext/issues/57327)) ([#57511](https://github.com/frappe/erpnext/issues/57511)) ([c0ac8aa](c0ac8aaf86))
* sync translations from crowdin ([#57741](https://github.com/frappe/erpnext/issues/57741)) ([285aec3](285aec3164))
* update stock variance account logic which defaults to default expense ([#57656](https://github.com/frappe/erpnext/issues/57656)) ([d09c04a](d09c04a623))
* use payment entry posting date for received amount exchange rate (backport [#57660](https://github.com/frappe/erpnext/issues/57660)) ([#57663](https://github.com/frappe/erpnext/issues/57663)) ([5595d1e](5595d1ed2f))
* validate account frozen date ([17aeb0b](17aeb0b55b))
* warn about existing draft orders before the supplier selection creates more ([93331a1](93331a1cf0))

### Features

* adjust the ordered quantity in the supplier selection dialog ([2c8c375](2c8c375ca6))
* alert when Required By falls back to today ([1adeb66](1adeb66bdc))
* auto-fill subscription accounting dimensions from plan with item fallback (backport [#57615](https://github.com/frappe/erpnext/issues/57615)) ([#57622](https://github.com/frappe/erpnext/issues/57622)) ([ebba4e9](ebba4e9958))
* order only the items ticked in the supplier selection dialog ([d6ee543](d6ee5436b8))
* select a supplier per item when creating Purchase Orders from Material Request ([95f7810](95f7810948))
* set one supplier across every item in the supplier selection dialog ([9b647be](9b647bed5c))
* show the UOM alongside the quantity in the supplier selection dialog ([5aaefec](5aaefec747))
* status based bar colors in Work Order gantt view (backport [#57634](https://github.com/frappe/erpnext/issues/57634)) ([#57636](https://github.com/frappe/erpnext/issues/57636)) ([2d03d80](2d03d80269))
2026-08-04 20:30:34 +00:00
Diptanil Saha
2769a8c69e Merge pull request #57781 from frappe/version-16-hotfix
chore: release v16
2026-08-05 01:58:06 +05:30
Frappe PR Bot
8378b6e203 chore(release): Bumped to Version 16.30.0
# [16.30.0](https://github.com/frappe/erpnext/compare/v16.29.0...v16.30.0) (2026-07-28)

### Bug Fixes

* add permission check for `get_item_details` (backport [#57515](https://github.com/frappe/erpnext/issues/57515)) ([#57551](https://github.com/frappe/erpnext/issues/57551)) ([7a606ab](7a606ab91c))
* bump item doctype modified timestamp so description change syncs on migrate ([2471c5c](2471c5ccbb))
* **crm:** align Opportunity status checks with Quotation statuses (backport [#57489](https://github.com/frappe/erpnext/issues/57489)) ([#57491](https://github.com/frappe/erpnext/issues/57491)) ([ad9870a](ad9870acc1))
* **crm:** clarify the reason why an opportunity cannot be declared as lost (backport [#57495](https://github.com/frappe/erpnext/issues/57495)) ([#57498](https://github.com/frappe/erpnext/issues/57498)) ([3ea19d8](3ea19d8eb1))
* detect the currency column by fieldtype in trends total row ([a30aac8](a30aac87bf))
* enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (backport [#57458](https://github.com/frappe/erpnext/issues/57458)) ([#57460](https://github.com/frappe/erpnext/issues/57460)) ([a0af717](a0af717234))
* enhance growth view filtering by validating period keys ([3f87836](3f87836536))
* force-delete repost data file during cleanup (backport [#57245](https://github.com/frappe/erpnext/issues/57245)) ([32baf6a](32baf6a47c))
* get reserved batch qty precision from settings ([95de237](95de2374ed))
* guard against missing is_your_company_address custom field on address ([2e6b4b5](2e6b4b5838))
* Incorrect creation time at the time cancelling an entry causing an issue especially same posting datetime  (backport [#57380](https://github.com/frappe/erpnext/issues/57380)) ([#57397](https://github.com/frappe/erpnext/issues/57397)) ([84813d7](84813d7f46))
* **item:** correct description on deferred revenue/expense ([ee12f8d](ee12f8d2d4))
* **manufacturing:** exclude corrective job cards from semi-FG aggregate ([ebc8482](ebc8482310))
* **manufacturing:** fall back to UOM Conversion Factor in Production Plan (backport [#57553](https://github.com/frappe/erpnext/issues/57553)) ([#57554](https://github.com/frappe/erpnext/issues/57554)) ([8f36753](8f36753705))
* **manufacturing:** scope BOM Creator tree children to the parent row ([339bb0b](339bb0b4ea)), closes [#57311](https://github.com/frappe/erpnext/issues/57311)
* **manufacturing:** sum semi-FG qty across split job cards ([c411b8e](c411b8e471))
* **manufacturing:** update cost of BOMs created via BOM Creator ([f1a0a5e](f1a0a5e1bf))
* map MT940 per-transaction reference from :61: customer_reference ([b9c4f79](b9c4f790bd))
* map pick list customer to delivery note when no sales order ([3f6501b](3f6501b4ff)), closes [#57412](https://github.com/frappe/erpnext/issues/57412)
* migrate stored AR/AP ageing filter to renamed field ([2e5fc38](2e5fc38179))
* pool batch slot values on every run, not only when negative ([5763378](5763378ee1))
* **quotation:** carry forward communications from opportunity (backport [#57507](https://github.com/frappe/erpnext/issues/57507)) ([#57509](https://github.com/frappe/erpnext/issues/57509)) ([f9b3e42](f9b3e42dcd))
* rebalance batch slot values at the pooled rate when driven negative ([8a50572](8a50572786))
* recalculate operating cost on hour rate change in routing ([c182b40](c182b4085b))
* recover failed POS closings (backport [#57203](https://github.com/frappe/erpnext/issues/57203)) ([#57572](https://github.com/frappe/erpnext/issues/57572)) ([9c946eb](9c946eb168))
* rename misleading filter labels in AR/AP reports ([6d856fa](6d856fa632))
* resolve backport conflicts in item doctype ([0ce5040](0ce50407f9))
* respect selected BOM when creating work order for variant item ([#57359](https://github.com/frappe/erpnext/issues/57359)) ([1132eb1](1132eb1a0f))
* respect user permissions in party dashboard company list ([c79f2e4](c79f2e45a8)), closes [frappe/erpnext#57428](https://github.com/frappe/erpnext/issues/57428)
* **setup:** scope manufacturing warehouse filters to company ([f05e8ed](f05e8ed0ce))
* skip stock expense GL entries for non-stock items ([#57518](https://github.com/frappe/erpnext/issues/57518)) ([861c50e](861c50e727))
* **stock:** narrow legacy serial ledger lookup by item (backport [#57499](https://github.com/frappe/erpnext/issues/57499)) ([#57506](https://github.com/frappe/erpnext/issues/57506)) ([59efe72](59efe7299a))
* stop storing raw title template on subcontracting orders ([0863c1e](0863c1e05c))
* **subcontracting:** release raw-material reservation when closing a subcontracting order ([ee75fac](ee75fac9d7))
* **test:** flaky test in exchange rate revaluation ([484ff8e](484ff8e349))
* typeerror in get_batches_by_oldest for mixed batch expiry ([bbe7580](bbe7580c9d))
* update formatting of growth view for FS report ([da3844c](da3844c4df))
* update operating cost when propagating workstation hour rate to routing ([#57504](https://github.com/frappe/erpnext/issues/57504)) ([39d5fd8](39d5fd84db))
* use company currency instead of global default in report (backport [#56561](https://github.com/frappe/erpnext/issues/56561)) ([9e5f77b](9e5f77b57c))

### Features

* block sales invoice submit when customer overdue exceeds threshold (backport [#57230](https://github.com/frappe/erpnext/issues/57230), [#57298](https://github.com/frappe/erpnext/issues/57298)) ([#57438](https://github.com/frappe/erpnext/issues/57438)) ([d3c5e86](d3c5e866f0)), closes [#52960](https://github.com/frappe/erpnext/issues/52960)
* book Expenses Added To Stock GL entries (backport [#57190](https://github.com/frappe/erpnext/issues/57190) + [#57475](https://github.com/frappe/erpnext/issues/57475)) ([#57503](https://github.com/frappe/erpnext/issues/57503)) ([68caa60](68caa60dfa))
* make Shipping Rule Cost Center optional with company default fallback (backport [#57355](https://github.com/frappe/erpnext/issues/57355)) ([#57385](https://github.com/frappe/erpnext/issues/57385)) ([bbd942c](bbd942c600))
2026-07-28 23:31:26 +00:00
Diptanil Saha
eaf95e5c36 Merge pull request #57548 from frappe/version-16-hotfix
chore: release v16
2026-07-29 04:59:39 +05:30
Frappe PR Bot
a5de60c357 chore(release): Bumped to Version 16.29.0
# [16.29.0](https://github.com/frappe/erpnext/compare/v16.28.0...v16.29.0) (2026-07-22)

### Bug Fixes

* add fetch from in production plan material request child table ([2852671](2852671cd5))
* added missing validations for `Dunning Type` (backport [#57224](https://github.com/frappe/erpnext/issues/57224)) ([#57227](https://github.com/frappe/erpnext/issues/57227)) ([3948951](3948951904))
* allow delivery when a batch is reserved across multiple sales orders (backport [#57169](https://github.com/frappe/erpnext/issues/57169)) ([56bbca0](56bbca0203))
* block changing Stock account type when stock ledger entries exist (backport [#57283](https://github.com/frappe/erpnext/issues/57283)) ([#57285](https://github.com/frappe/erpnext/issues/57285)) ([a26296c](a26296ca50))
* consider min order qty in the purchase/transfer flow of production plan (backport [#57204](https://github.com/frappe/erpnext/issues/57204)) ([cbd4f93](cbd4f93f3d))
* correct typo in allow_negative_stock parameter ([e2be05e](e2be05e48b))
* dont overwrite rate with 0 if not found ([aa08f75](aa08f753b4))
* exclude fully-delivered reservations from the conflict message ([555c607](555c607f2f))
* exclude transferred_qty from work order item to pick list item mapping ([c466b49](c466b49d09)), closes [#57253](https://github.com/frappe/erpnext/issues/57253) [#57236](https://github.com/frappe/erpnext/issues/57236) [#56596](https://github.com/frappe/erpnext/issues/56596)
* hide job card field in purchase order item ([20f6ac8](20f6ac81b9))
* keep Standard Cost stock value in step with the standard rate ([fe65882](fe65882e59))
* name every conflicting voucher in the reserved batch error ([839fd5e](839fd5e3b6))
* parallel reposting stalls between scheduler ticks (backport [#57220](https://github.com/frappe/erpnext/issues/57220)) ([#57248](https://github.com/frappe/erpnext/issues/57248)) ([a3bfded](a3bfdede06))
* pass ctx keys get_price_list_rate_for reads, skip rate update on insert ([c3aea9c](c3aea9ca9c))
* **payments:** ensure `payments` app installed on the site in `payment_app_import_guard` (backport [#57342](https://github.com/frappe/erpnext/issues/57342)) ([#57344](https://github.com/frappe/erpnext/issues/57344)) ([2c8c076](2c8c076f6e))
* permission issue (backport [#57112](https://github.com/frappe/erpnext/issues/57112)) (backport [#57142](https://github.com/frappe/erpnext/issues/57142)) ([#57184](https://github.com/frappe/erpnext/issues/57184)) ([0817d10](0817d1064c))
* project % complete field allowing modification when manual method (backport [#57274](https://github.com/frappe/erpnext/issues/57274)) ([#57276](https://github.com/frappe/erpnext/issues/57276)) ([0d53f1a](0d53f1adb7))
* **project:** improved access control for project users (backport [#56675](https://github.com/frappe/erpnext/issues/56675)) ([#57181](https://github.com/frappe/erpnext/issues/57181)) ([b1adec7](b1adec7e9e))
* **report:** handle nonetype error in timesheet billing summary grouping logic ([b7cf3bf](b7cf3bf641))
* rescale batch FIFO slot values on stock reconciliation ([6b3b03f](6b3b03fcd8))
* rescale stock ageing FIFO slot values on stock reconciliation ([c9394c0](c9394c030f))
* resolve float precision before streaming stock ledger entries ([0f25254](0f252542c3))
* restrict jinja globals in process statement of accounts templates (backport [#56458](https://github.com/frappe/erpnext/issues/56458)) ([#57232](https://github.com/frappe/erpnext/issues/57232)) ([5f6952b](5f6952b15c))
* revalue batch reco slots only when the entry covers the full batch ([a4bf506](a4bf50656a))
* round outstanding qty guard consistently with the conflict gate ([f3e1b3f](f3e1b3fca7))
* set correct currency in supplier quotation net rate field ([e24ef84](e24ef847dc))
* show transaction currency symbol in Payment Request schedule dialog and reference table (backport [#57050](https://github.com/frappe/erpnext/issues/57050)) ([#57312](https://github.com/frappe/erpnext/issues/57312)) ([429b58b](429b58b833))
* **stock:** correct stock ageing value for moving average and lifo items ([#56693](https://github.com/frappe/erpnext/issues/56693)) ([02549ef](02549ef8c4))
* strip account number when building account name in COA importer ([e1ebfa7](e1ebfa7163))
* sync process loss percentage when fg qty changes ([beeffee](beeffee8f9))
* sync translations from crowdin ([#57187](https://github.com/frappe/erpnext/issues/57187)) ([827831a](827831a247))
* use system float precision for batch qty comparison ([92217b2](92217b2c45))
* validate buying price list on material request and update item rates on change ([025b2f2](025b2f2922))

### Features

* add on hold status to project ([57a49ce](57a49ce168))
* recalculate valuation rate and stock value from Bin ([a38cbfc](a38cbfc88e))
* **stock:** automatically link portal users to their associated contact profiles for customers and suppliers ([134d63d](134d63de78))
* **stock:** expose all Bin qty fields in Stock Summary and Stock Projected Qty ([0bdf258](0bdf258888))
2026-07-22 00:17:03 +00:00
Diptanil Saha
264bfa188b Merge pull request #57324 from frappe/version-16-hotfix
chore: release v16
2026-07-22 05:45:21 +05:30
Frappe PR Bot
de591661b9 chore(release): Bumped to Version 16.28.0
# [16.28.0](https://github.com/frappe/erpnext/compare/v16.27.0...v16.28.0) (2026-07-15)

### Bug Fixes

* **accounts:** added permission checks on `get_account_balances_coa` (backport [#57107](https://github.com/frappe/erpnext/issues/57107)) ([#57122](https://github.com/frappe/erpnext/issues/57122)) ([e8a5325](e8a532587d))
* allow barcode scan to add and increment items in pick list ([00a646a](00a646ac25))
* apply precision fix inline for v16-hotfix, drop develop-only asset_service refactor ([d3a8e91](d3a8e91cda))
* **asset:** allow asset repair creation for fully depreciated assets ([8658039](8658039e9a))
* **budget-variance:** correct month shift in comparison chart ([703e9a7](703e9a728c))
* guard company logo lookup in default letterheads ([7b54314](7b543142a2))
* **journal-entry:** fetch outstanding on foreign currency ([ec782ee](ec782ee20d))
* **manufacturing:** preserve job card transferred quantity ([d7e9321](d7e9321746))
* match depreciation schedule rows at currency precision to avoid duplicate JEs ([a1fae95](a1fae959ed))
* prevent duplicate material request items in purchase order ([4d951c1](4d951c1cf8))
* set explicit table and logo widths in grey letterhead ([0d80fef](0d80fef3bf))
* **stock:** fix sqlparse token limit in get_bundle_wise_serial_nos ([5a99dd6](5a99dd6016))
* **stock:** set stock_uom on transferred Stock Reservation Entries ([5991ecf](5991ecfa3d))
* **stock:** show qty (company) and qty (warehouse) in sales transactions ([0d9ace7](0d9ace7ab8))
* **tnc:** `get_terms_and_conditions` render_template with `safe_exec` (backport [#56944](https://github.com/frappe/erpnext/issues/56944)) ([#56977](https://github.com/frappe/erpnext/issues/56977)) ([40c85a0](40c85a0087))

### Features

* **balance-sheet:** implement execute_synced_report with full parity to normal report ([636bcbe](636bcbedc0))
* **general-ledger:** implement execute_synced_report with full parity to normal report ([19ec095](19ec095ff8))
* **profit-and-loss:** implement execute_synced_report with full parity to normal report ([21ddb00](21ddb00e20))
* **trial-balance:** implement execute_duckdb with full parity to normal report ([228418b](228418b05f))
* weekly auto-repost of incorrect stock valuation entries (backport [#56637](https://github.com/frappe/erpnext/issues/56637)) ([#56700](https://github.com/frappe/erpnext/issues/56700)) ([bf1b7f2](bf1b7f2bea))
2026-07-15 02:24:51 +00:00
ruthra kumar
9a7e796fd2 Merge pull request #57161 from frappe/version-16-hotfix
chore: release v16
2026-07-15 06:17:14 +05:30
Frappe PR Bot
9d5c7605b8 chore(release): Bumped to Version 16.27.0
# [16.27.0](https://github.com/frappe/erpnext/compare/v16.26.2...v16.27.0) (2026-07-13)

### Bug Fixes

* **`frappe_crm_api`:** handle failure for `after_app_install` and `after_app_uninstall` ([eac3afc](eac3afcd88))
* **accounts:** clear clearance date when amending reconciled voucher ([6f28135](6f281356a6)), closes [#54909](https://github.com/frappe/erpnext/issues/54909)
* **accounts:** retain invoice table on opening invoice creation error (backport [#56353](https://github.com/frappe/erpnext/issues/56353)) ([#57058](https://github.com/frappe/erpnext/issues/57058)) ([4136ca5](4136ca55a7))
* add page numbers to print format footer ([0371e8e](0371e8eaf0))
* added permission checks on various whitelisted functions (backport [#56745](https://github.com/frappe/erpnext/issues/56745)) ([#56946](https://github.com/frappe/erpnext/issues/56946)) ([7ce1289](7ce1289c10))
* address review comments ([056195c](056195ce07))
* auto fetch serial no from previous operation output (backport to v16) ([#56861](https://github.com/frappe/erpnext/issues/56861)) ([17f2de4](17f2de42f3))
* **banking:** allow negative balance in bank statement import (backport [#56959](https://github.com/frappe/erpnext/issues/56959)) ([#57054](https://github.com/frappe/erpnext/issues/57054)) ([835e32c](835e32cec7))
* **banking:** handle blank password protected PDFs and negative amounts in CR/DR columns (backport [#56690](https://github.com/frappe/erpnext/issues/56690)) ([#56694](https://github.com/frappe/erpnext/issues/56694)) ([19c318d](19c318df68))
* **banking:** use custom renderer for translated strings and parser for rules (backport [#56643](https://github.com/frappe/erpnext/issues/56643)) ([#56647](https://github.com/frappe/erpnext/issues/56647)) ([2cb577b](2cb577b912))
* block serialized to non-serialized item change when SABB exists (backport [#56773](https://github.com/frappe/erpnext/issues/56773)) ([#56775](https://github.com/frappe/erpnext/issues/56775)) ([19d03fe](19d03fee46))
* clear stray permission message when item dashboard has no warehouse access ([a39753e](a39753ee08))
* **company:** ignore user permissions for link fields having link to `Account` and `Cost Center` (backport [#56748](https://github.com/frappe/erpnext/issues/56748)) ([#56795](https://github.com/frappe/erpnext/issues/56795)) ([003b655](003b6554c4))
* correct filter handling in Sales Person-wise Transaction Summary + tests (backport [#56783](https://github.com/frappe/erpnext/issues/56783)) ([#56908](https://github.com/frappe/erpnext/issues/56908)) ([24f0989](24f0989ac1))
* **crm_settings:** skip allowed users check when frappe crm is installed locally ([dfe4d5a](dfe4d5ab73))
* display outstanding amount using company default currency (backport [#56785](https://github.com/frappe/erpnext/issues/56785)) ([#57009](https://github.com/frappe/erpnext/issues/57009)) ([bdba4c8](bdba4c8091))
* don't attempt to create SABB for non-serialized / non-batch items (backport [#56627](https://github.com/frappe/erpnext/issues/56627)) ([#56803](https://github.com/frappe/erpnext/issues/56803)) ([8c56a5a](8c56a5ac0c))
* don't treat batch slot at FIFO queue head as qty slot ([20f6dd0](20f6dd0224))
* enforce read permission on ledger preview endpoints ([#57039](https://github.com/frappe/erpnext/issues/57039)) ([5642359](5642359c10))
* fetch payment entry reference amounts from invoice (backport [#56928](https://github.com/frappe/erpnext/issues/56928)) ([#57042](https://github.com/frappe/erpnext/issues/57042)) ([67c85ef](67c85ef0af))
* FIFO queue checks and incorrect entries filter in stock ledger reports ([3571512](3571512101)), closes [#43619](https://github.com/frappe/erpnext/issues/43619)
* **financial_statement:** render columnar financial statements instea… (backport [#56921](https://github.com/frappe/erpnext/issues/56921)) ([#57053](https://github.com/frappe/erpnext/issues/57053)) ([e6a6c13](e6a6c13355))
* for purchases do voucher based reposting (backport [#56601](https://github.com/frappe/erpnext/issues/56601)) ([#56608](https://github.com/frappe/erpnext/issues/56608)) ([e1e6176](e1e6176ddc))
* guard against missing DocType in onboarding steps patch ([#56804](https://github.com/frappe/erpnext/issues/56804)) ([f72289e](f72289e27c))
* **item-attribute:** clear attribute values when marking numeric ([374b340](374b340e73))
* **item:** error on uncommitted input and escape values in variant dialog ([04c834d](04c834d6a9))
* **item:** rework multiple variant dialog for large numeric ranges ([025d0cd](025d0cd7f3))
* make represents company field in purchase invoice ignore user permissions ([4aed77d](4aed77df0c))
* make trend report based-on and group-by column labels translatable ([b8199d8](b8199d88b6))
* map stock_qty in apply_price_list_on_item (backport [#56869](https://github.com/frappe/erpnext/issues/56869)) ([#57052](https://github.com/frappe/erpnext/issues/57052)) ([240fb2c](240fb2c4b8))
* partial delivery note against pick list (backport [#56985](https://github.com/frappe/erpnext/issues/56985)) ([#57006](https://github.com/frappe/erpnext/issues/57006)) ([20255a8](20255a8a7f))
* **patch:** moved create_company_custom_fields from `pre_model_sync` to `post_model_sync` (backport [#56962](https://github.com/frappe/erpnext/issues/56962)) ([#56965](https://github.com/frappe/erpnext/issues/56965)) ([14efd14](14efd14384))
* **payment reconciliation:** honour user permissions on accounting di… ([#56560](https://github.com/frappe/erpnext/issues/56560)) ([8eb92b8](8eb92b8b18))
* precision issue causing reconciliation error ([#54043](https://github.com/frappe/erpnext/issues/54043)) ([20df15b](20df15b3ac))
* prevent repeatable read related concurrency errors ([019b02a](019b02adcd))
* race condition and repeatable read in process pcv ([6d9f5fa](6d9f5fac76))
* recompute transferred qty before deciding work order status ([bc952f4](bc952f40d9))
* remove company default on cost center in stock entry detail ([a1fe6ce](a1fe6cee5d))
* remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport [#57018](https://github.com/frappe/erpnext/issues/57018)) ([#57061](https://github.com/frappe/erpnext/issues/57061)) ([edfbc71](edfbc71135))
* rename variant item_code/item_name when attribute abbreviation changes ([97cd1e7](97cd1e7144))
* render letter head footer in print formats ([fa4d32d](fa4d32dcdb))
* replay immutable SLE qty for serial/batch bundle valuation (backport [#56814](https://github.com/frappe/erpnext/issues/56814)) ([#56835](https://github.com/frappe/erpnext/issues/56835)) ([2264e25](2264e25cc6))
* resolve conflicts ([3aad7fe](3aad7fee24))
* restore Save button on reverse journal entry (backport [#56770](https://github.com/frappe/erpnext/issues/56770)) ([#56772](https://github.com/frappe/erpnext/issues/56772)) ([4573cd1](4573cd15a9))
* **selling:** update sales order per billed on credit note submission ([224cf19](224cf19f33))
* set conversion_rate on quotation created from customer ([26905bc](26905bc142))
* show only template items in Variant Of filter ([bebe011](bebe011636))
* skip item prices tab render for users without item price read access ([c964811](c964811293))
* skip stock reservation for opted-out production plans ([17598e2](17598e2626)), closes [#56798](https://github.com/frappe/erpnext/issues/56798)
* **stock:** backfill transferred qty for existing pick lists ([903d78c](903d78cc43))
* **stock:** link job card in stock entry created from pick list (backport [#57031](https://github.com/frappe/erpnext/issues/57031)) ([755b9cc](755b9ccbc3))
* **stock:** pick list serial batch posting date ([#57015](https://github.com/frappe/erpnext/issues/57015)) ([7e46be2](7e46be2a33)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** propagate project from job card to stock entry ([51d2077](51d2077eed))
* **stock:** support quality inspection for stock entry by purpose (backport [#56446](https://github.com/frappe/erpnext/issues/56446)) ([40ca3b5](40ca3b5e5d))
* update BOM operations when routing is changed ([b2e6a39](b2e6a39743))
* update events order by date asc (backport [#56963](https://github.com/frappe/erpnext/issues/56963)) ([#57056](https://github.com/frappe/erpnext/issues/57056)) ([e2fd061](e2fd061b3d))
* update modified timestamp in item.json ([f602ee0](f602ee0e73))
* use live source warehouse valuation for internal transfer purchase receipts (backport [#56431](https://github.com/frappe/erpnext/issues/56431)) ([#56677](https://github.com/frappe/erpnext/issues/56677)) ([1da28f2](1da28f2278))
* validate planned end date is not before planned start date in work order ([745baad](745baad0d1))
* validate reverse GL entries on current date under immutable ledger (backport [#56709](https://github.com/frappe/erpnext/issues/56709)) ([#56752](https://github.com/frappe/erpnext/issues/56752)) ([eea4756](eea4756ce9))
* validate template and its variant in the same Pricing Rule ([159a253](159a2538da))

### Features

* capture user persona during setup (backport [#56705](https://github.com/frappe/erpnext/issues/56705)) ([#56707](https://github.com/frappe/erpnext/issues/56707)) ([d9aa449](d9aa4491f1))
* **crm_settings:** auto-update crm sync settings on frappe crm install and uninstall ([cb2a930](cb2a930a32))
* explain FIFO allocation of fixed Discount Amount on Sales Order ([#56436](https://github.com/frappe/erpnext/issues/56436)) ([62fed1d](62fed1d562))
* **manufacturing:** create material request for raw materials from work order ([#56980](https://github.com/frappe/erpnext/issues/56980)) ([144f92d](144f92d58e))
* **sla:** filter service level agreement link by document type (backport [#56954](https://github.com/frappe/erpnext/issues/56954)) ([#56956](https://github.com/frappe/erpnext/issues/56956)) ([393b5d1](393b5d1f74))
* **stock:** support partial transfer from pick list ([af495ed](af495ed253))

### Performance Improvements

* avoid per-row Warehouse doc fetches in auto reorder job ([39b5e12](39b5e12305))
* batch bin lookups in delivery note stock update ([697e2c0](697e2c0b66))
2026-07-13 07:41:31 +00:00
Diptanil Saha
f94eee3197 Merge pull request #56931 from frappe/version-16-hotfix
chore: release v16
2026-07-13 12:09:52 +05:30
38 changed files with 177 additions and 2241 deletions

View File

@@ -23,19 +23,6 @@ jobs:
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.1
js-unit-tests:
name: js unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Run JS unit tests
run: yarn test:js
semgrep:
name: semgrep
runs-on: ubuntu-latest

View File

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

View File

@@ -7,7 +7,6 @@ frappe.ui.form.on("Bank Statement Import", {
return {
filters: {
company: doc.company,
is_company_account: 1,
},
};
});

View File

@@ -151,7 +151,7 @@ function refresh_payments(payments, frm) {
mode_of_payment: p.mode_of_payment,
opening_amount: 0,
expected_amount: p.amount,
closing_amount: p.amount,
closing_amount: 0,
});
}
});

View File

@@ -424,7 +424,6 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
set_transaction_type(args_copy)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)

View File

@@ -501,7 +501,6 @@ class SalesInvoice(SellingController):
self.validate_standalone_serial_nos_customer()
self.update_stock_reservation_entries()
self.update_stock_ledger()
self.validate_produced_serial_nos_against_reservation()
self.split_asset_based_on_sale_qty()

View File

@@ -5296,34 +5296,6 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
@ERPNextTestSuite.change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_update_stock_restricted_to_reserved_produced_serial_nos(self):
from erpnext.selling.doctype.sales_order.sales_order import (
make_sales_invoice as make_si_from_so,
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
make_so_with_reserved_produced_serial_no,
)
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
def make_si(serial_no):
si = make_si_from_so(so.name)
si.update_stock = 1
si.items[0].warehouse = so.items[0].warehouse
si.items[0].use_serial_batch_fields = 1
si.items[0].serial_no = serial_no
return si.save()
frappe.db.savepoint("unreserved_serial_no")
si = make_si(unreserved[0])
self.assertRaises(frappe.ValidationError, si.submit)
frappe.db.rollback(save_point="unreserved_serial_no")
si = make_si(reserved[0])
si.submit()
self.assertEqual(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle), reserved)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -423,15 +423,7 @@ def get_invoices(filters, additional_query_columns):
if filters.get("supplier"):
query = query.where(pi.supplier == filters.supplier)
if filters.get("supplier_group"):
# read the group from the supplier master, to match the Supplier Group column
supplier = frappe.qb.DocType("Supplier")
query = query.where(
pi.supplier.isin(
frappe.qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == filters.supplier_group)
)
)
query = query.where(pi.supplier_group == filters.supplier_group)
query = get_conditions(filters, query, "Purchase Invoice")

View File

@@ -117,23 +117,6 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.credit, 600)
self.assertEqual(first_row.balance, 500)
def test_supplier_group_filter_uses_supplier_master(self):
# invoices created before the supplier_group field existed have it blank
pi = make_purchase_invoice()
pi.db_set("supplier_group", None, update_modified=False)
supplier_group = frappe.db.get_value("Supplier", pi.supplier, "supplier_group")
filters = frappe._dict(
company="_Test Company 6",
from_date=add_months(today(), -1),
to_date=today(),
supplier_group=supplier_group,
)
rows = [frappe._dict(row) for row in execute(filters)[1] if row.get("voucher_no") == pi.name]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0].supplier_group, supplier_group)
def make_purchase_invoice():
from erpnext.accounts.doctype.account.test_account import create_account

View File

@@ -796,25 +796,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
po = create_purchase_order(qty=3.4, do_not_save=True)
self.assertRaises(UOMMustBeIntegerError, po.insert)
def test_uom_integer_check_tolerates_conversion_dust(self):
from erpnext.utilities.transaction_base import UOMMustBeIntegerError
item_doc = make_item(properties={"stock_uom": "Nos"})
item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.6})
item_doc.save()
item = item_doc.name
precision = frappe.get_precision("Purchase Order Item", "stock_qty")
po = create_purchase_order(item_code=item, qty=flt(2000 / 0.6, precision), do_not_save=1)
po.items[0].uom = "Kg"
po.items[0].conversion_factor = 0.6
po.insert()
fractional = create_purchase_order(item_code=item, qty=3333.9, do_not_save=1)
fractional.items[0].uom = "Kg"
fractional.items[0].conversion_factor = 0.6
self.assertRaises(UOMMustBeIntegerError, fractional.insert)
def test_ordered_qty_for_closing_po(self):
bin = frappe.get_all(
"Bin",

View File

@@ -16,7 +16,6 @@ from pypika import Order
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.doctype.item.item_search import get_item_search_candidates
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
from erpnext.stock.utils import get_combine_datetime
@@ -177,15 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict | str | None,
as_dict: bool = False,
):
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Item"
conditions = []
@@ -216,7 +207,6 @@ def item_query(
]
if field not in searchfields
]
searched_fields = list(searchfields)
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict):
@@ -273,17 +263,7 @@ def item_query(
if frappe.db.estimate_count(doctype) < 50000:
# scan description only if items are less than 50000
description_cond = "or tabItem.description LIKE %(txt)s"
searched_fields.append("description")
candidate_cond = ""
candidates = get_item_search_candidates(txt, searched_fields)
if candidates is not None:
if not candidates:
return [] if as_dict else ()
candidate_cond = "and tabItem.name in %(candidates)s"
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-sql-format-injection
return frappe.db.sql(
"""select
tabItem.name {columns}
@@ -294,7 +274,7 @@ def item_query(
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s)
{description_cond})
{fcond} {mcond} {candidate_cond}
{fcond} {mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, item_name), locate(%(_txt)s, item_name), 99999),
@@ -306,7 +286,6 @@ def item_query(
fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
mcond=get_match_cond(doctype).replace("%", "%%"),
description_cond=description_cond,
candidate_cond=candidate_cond,
),
{
"today": nowdate(),
@@ -314,7 +293,6 @@ def item_query(
"_txt": txt.replace("%", ""),
"start": start,
"page_len": page_len,
"candidates": tuple(candidates or ()),
},
as_dict=as_dict,
)

View File

@@ -904,77 +904,6 @@ class SellingController(StockController):
title=_("Not Allowed"),
)
def validate_produced_serial_nos_against_reservation(self):
"""Restrict delivery to the serial nos reserved for a Sales Order Item with ensure delivery by serial no."""
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_serial_nos_for_voucher_detail_nos,
)
if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
rows = [d for d in self.items if d.get(so_field) and d.so_detail]
if not rows:
return
flagged_so_details = frappe.get_all(
"Sales Order Item",
filters={
"name": ("in", [d.so_detail for d in rows]),
"ensure_delivery_based_on_produced_serial_no": 1,
},
pluck="name",
)
rows = [d for d in rows if d.so_detail in flagged_so_details]
if not rows:
return
reserved_serial_nos = get_sre_reserved_serial_nos_for_voucher_detail_nos(
"Sales Order", flagged_so_details
)
bundle_map = dict(
frappe.get_all(
rows[0].doctype,
filters={"name": ("in", [d.name for d in rows])},
fields=["name", "serial_and_batch_bundle"],
as_list=True,
)
)
bundle_serial_nos = frappe._dict()
if bundles := [b for b in bundle_map.values() if b]:
for entry in frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
fields=["parent", "serial_no"],
):
bundle_serial_nos.setdefault(entry.parent, []).append(entry.serial_no)
for row in rows:
if not reserved_serial_nos.get(row.so_detail):
frappe.throw(
_(
"Row #{0}: Delivery of Item {1} is ensured by produced Serial No, but no Serial No is reserved against Sales Order {2}. Reserve the produced Serial Nos from the Sales Order."
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.get(so_field))),
title=_("Serial No Not Reserved"),
)
bundle = bundle_map.get(row.name)
serial_nos = bundle_serial_nos.get(bundle, []) if bundle else get_serial_nos(row.serial_no)
if invalid_serial_nos := [
sn for sn in serial_nos if sn not in reserved_serial_nos[row.so_detail]
]:
frappe.throw(
_(
"Row #{0}: Serial No {1} is not reserved against Sales Order {2}. Deliver only the Serial Nos produced and reserved for it."
).format(
row.idx, frappe.bold(", ".join(invalid_serial_nos)), frappe.bold(row.get(so_field))
),
title=_("Serial No Not Reserved"),
)
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""

View File

@@ -347,8 +347,6 @@ period_closing_doctypes = [
"Subcontracting Receipt",
]
sqlite_search = ["erpnext.stock.doctype.item.item_search.ItemSearch"]
doc_events = {
"*": {
"validate": [
@@ -359,10 +357,6 @@ doc_events = {
tuple(period_closing_doctypes): {
"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
},
"Item": {
"on_update": "erpnext.stock.doctype.item.item_search.reindex_item",
"after_rename": "erpnext.stock.doctype.item.item_search.reindex_renamed_item",
},
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",

View File

@@ -1508,7 +1508,6 @@ def get_bom_items_as_dict(
bom_item.idx,
item.item_name,
sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty,
sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(qty)s as stock_qty,
item.image,
bom.project,
item.stock_uom,
@@ -1585,12 +1584,11 @@ def get_bom_items_as_dict(
if item.operation:
key = (item.item_code, item.operation)
stock_qty = item.pop("stock_qty")
if item.get("is_phantom_item"):
data = get_bom_items_as_dict(
item.get("bom_no"),
company,
qty=stock_qty,
qty=item.get("qty"),
fetch_exploded=fetch_exploded,
fetch_secondary_items=fetch_secondary_items,
include_non_stock_items=include_non_stock_items,

View File

@@ -84,36 +84,6 @@ class TestBOM(ERPNextTestSuite):
),
)
@timeout
def test_get_items_explodes_phantom_row_by_stock_qty(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
rm = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
kit = make_item(
properties={"is_stock_item": 0, "uoms": [{"uom": "Box", "conversion_factor": 5}]}
).name
phantom_bom = make_bom(item=kit, raw_materials=[rm], do_not_save=True)
phantom_bom.is_phantom_bom = 1
phantom_bom.save()
phantom_bom.submit()
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
bom = make_bom(item=fg_item, raw_materials=[kit], do_not_save=True)
bom.items[0].update({"qty": 2, "uom": "Box", "bom_no": phantom_bom.name})
bom.save()
bom.submit()
for fetch_qty_in_stock_uom in (True, False):
items_dict = get_bom_items_as_dict(
bom.name,
"_Test Company",
qty=1,
fetch_exploded=0,
fetch_qty_in_stock_uom=fetch_qty_in_stock_uom,
)
self.assertEqual(flt(items_dict[rm].qty), 10.0)
@timeout
def test_get_bom_diff_checks_both_boms(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_diff

View File

@@ -1595,99 +1595,6 @@ class TestJobCard(ERPNextTestSuite):
8,
)
def test_semi_fg_secondary_items_across_split_job_cards(self):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.stock.doctype.item.test_item import make_item
warehouse = "Stores - _TC"
rm = make_item("Split JC Scrap RM", {"is_stock_item": 1, "valuation_rate": 100}).name
fg = make_item("Split JC Scrap FG", {"is_stock_item": 1}).name
scrap = make_item("Split JC Scrap", {"is_stock_item": 1, "valuation_rate": 5}).name
fg_bom = frappe.new_doc(
"BOM",
company="_Test Company",
item=fg,
quantity=1,
with_operations=1,
track_semi_finished_goods=1,
)
fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1})
fg_bom.append("secondary_items", {"item_code": scrap, "qty": 1, "secondary_item_type": "Scrap"})
operation = {
"operation": "Split JC Scrap Op",
"workstation": "_Test Workstation A",
"finished_good": fg,
"finished_good_qty": 1,
"is_final_finished_good": 1,
"sequence_id": 1,
"time_in_mins": 60,
"source_warehouse": warehouse,
"fg_warehouse": warehouse,
"skip_material_transfer": 1,
}
make_workstation(operation)
make_operation(operation)
fg_bom.append("operations", operation)
fg_bom.insert()
fg_bom.submit()
work_order = make_wo_order_test_record(
item=fg,
qty=10,
source_warehouse=warehouse,
fg_warehouse=warehouse,
bom_no=fg_bom.name,
skip_transfer=1,
do_not_save=True,
)
work_order.operations[0].time_in_mins = 60
work_order.save()
work_order.submit()
make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100)
job_card = frappe.get_doc(
"Job Card", frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name")
)
job_card.for_quantity = 5
job_card.secondary_items[0].stock_qty = 5
job_card.append(
"time_logs",
{"from_time": "2024-02-01 08:00:00", "to_time": "2024-02-01 09:00:00", "completed_qty": 5},
)
job_card.save()
job_card.submit()
frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()).submit()
make_job_card(
work_order.name,
[
{
"name": work_order.operations[0].name,
"operation": "Split JC Scrap Op",
"qty": 5,
"pending_qty": 5,
"skip_material_transfer": 1,
}
],
)
job_card = frappe.get_doc(
"Job Card", frappe.db.get_value("Job Card", {"work_order": work_order.name, "docstatus": 0})
)
job_card.append(
"time_logs",
{"from_time": "2024-02-02 08:00:00", "to_time": "2024-02-02 09:00:00", "completed_qty": 5},
)
job_card.save()
job_card.submit()
stock_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
scrap_qty = sum(row.qty for row in stock_entry.items if row.item_code == scrap)
self.assertEqual(scrap_qty, 5)
def test_semi_fg_process_loss_rolls_up_to_work_order(self):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -595,14 +595,8 @@ class ProductionPlan(Document):
data.db_update()
self.calculate_total_produced_qty()
self.update_status_and_bin_qty()
def update_status_and_bin_qty(self):
previous_status = self.status
self.set_status()
self.db_set("status", self.status)
if previous_status != self.status and "Completed" in (previous_status, self.status):
self.update_bin_qty()
def on_submit(self):
self.update_bin_qty()
@@ -684,7 +678,11 @@ class ProductionPlan(Document):
return so_wise_planned_qty
def update_bin_qty(self):
self.update_raw_material_bin_qty()
for d in self.mr_items:
if d.warehouse:
bin_name = get_or_make_bin(d.item_code, d.warehouse)
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
for d in self.sub_assembly_items:
if d.fg_warehouse and d.type_of_manufacturing == "In House":
@@ -692,13 +690,6 @@ class ProductionPlan(Document):
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_for_sub_assembly()
def update_raw_material_bin_qty(self, item_codes: set[str] | None = None):
for d in self.mr_items:
if d.warehouse and (item_codes is None or d.item_code in item_codes):
bin_name = get_or_make_bin(d.item_code, d.warehouse)
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
def delete_draft_work_order(self):
for d in frappe.get_all(
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
@@ -709,9 +700,6 @@ class ProductionPlan(Document):
def set_status(self, close: bool | None = None, update_bin: bool = False):
self.check_permission("write")
if close is None and self.status == "Closed":
return
self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus)
if close:
@@ -2173,88 +2161,51 @@ def set_default_warehouses(row, default_warehouses):
def get_reserved_qty_for_production_plan(item_code, warehouse):
plan_reservations = _get_plan_reservations(item_code)
if not plan_reservations:
return None
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
work_order_reservations = _get_work_order_reservations(item_code, list(plan_reservations))
reserved_qty = 0.0
for plan, plan_qty_by_warehouse in plan_reservations.items():
reserved_qty += _get_remaining_reserved_qty(
plan_qty_by_warehouse, work_order_reservations.get(plan, {}), warehouse
)
return reserved_qty
def _get_remaining_reserved_qty(plan_qty_by_warehouse, work_order_qty_by_warehouse, warehouse):
remaining_qty_by_warehouse = {
plan_warehouse: max(qty - work_order_qty_by_warehouse.get(plan_warehouse, 0.0), 0.0)
for plan_warehouse, qty in plan_qty_by_warehouse.items()
}
total_remaining_qty = sum(remaining_qty_by_warehouse.values())
if not total_remaining_qty:
return 0.0
matched_qty = sum(plan_qty_by_warehouse.values()) - total_remaining_qty
unmatched_qty = min(sum(work_order_qty_by_warehouse.values()) - matched_qty, total_remaining_qty)
remaining_qty = remaining_qty_by_warehouse.get(warehouse, 0.0)
return remaining_qty - remaining_qty * unmatched_qty / total_remaining_qty
def _get_plan_reservations(item_code):
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
non_completed_production_plans = get_non_completed_production_plans()
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(
table.name.as_("production_plan"),
child.warehouse,
Sum(
Case().when(child.quantity == 0, child.required_bom_qty).else_(child.quantity)
* child.conversion_factor
).as_("reserved_qty"),
)
)
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
& (child.warehouse == warehouse)
& (table.status.notin(["Completed", "Closed"]))
)
.groupby(table.name, child.warehouse)
)
return _group_by_plan_and_warehouse(query)
if non_completed_production_plans:
query = query.where(table.name.isin(non_completed_production_plans))
def _get_work_order_reservations(item_code, plan_names):
work_order = frappe.qb.DocType("Work Order")
work_order_item = frappe.qb.DocType("Work Order Item")
query = (
frappe.qb.from_(work_order)
.from_(work_order_item)
.select(
work_order.production_plan,
work_order_item.source_warehouse.as_("warehouse"),
Sum(work_order_item.required_qty).as_("reserved_qty"),
query = query.run()
if not query or query[0][0] is None:
return None
reserved_qty_for_production_plan = flt(query[0][0])
reserved_qty_for_production = flt(
get_reserved_qty_for_production(
item_code, warehouse, non_completed_production_plans, check_production_plan=True
)
.where(
(work_order_item.item_code == item_code)
& (work_order_item.parent == work_order.name)
& (work_order.docstatus == 1)
& (IfNull(work_order_item.source_warehouse, "") != "")
& work_order.production_plan.isin(plan_names)
)
.groupby(work_order.production_plan, work_order_item.source_warehouse)
)
return _group_by_plan_and_warehouse(query)
if reserved_qty_for_production > reserved_qty_for_production_plan:
return 0.0
def _group_by_plan_and_warehouse(query):
reservations = {}
for row in query.run(as_dict=True):
reservations.setdefault(row.production_plan, {})[row.warehouse] = flt(row.reserved_qty)
return reservations
return reserved_qty_for_production_plan - reserved_qty_for_production
def get_non_completed_production_plans():

View File

@@ -2004,180 +2004,6 @@ class TestProductionPlan(ERPNextTestSuite):
self.assertEqual(after_qty, before_qty)
def test_plan_reservation_offsets_work_order_in_another_warehouse(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_production_plan,
)
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
plan_warehouse = "_Test Warehouse - _TC"
work_order_warehouse = "_Test Warehouse 1 - _TC"
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=plan_warehouse)
plan = create_production_plan(item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1)
self.assertEqual(get_reserved_qty_for_production_plan(rm_item, plan_warehouse), 10)
bin_name = frappe.db.get_value("Bin", {"item_code": rm_item, "warehouse": plan_warehouse}, "name")
bin = frappe.get_doc("Bin", bin_name)
self.assertEqual(bin.reserved_qty_for_production_plan, 10)
projected_qty = bin.projected_qty
work_order = submit_work_order_from_plan(plan, 5, work_order_warehouse)
self.assertEqual(get_reserved_qty_for_production_plan(rm_item, plan_warehouse), 5)
bin.reload()
self.assertEqual(bin.reserved_qty_for_production_plan, 5)
self.assertEqual(bin.projected_qty, projected_qty + 5)
work_order.cancel()
bin.reload()
self.assertEqual(bin.reserved_qty_for_production_plan, 10)
self.assertEqual(bin.projected_qty, projected_qty)
def test_plan_reservation_ignores_work_orders_of_other_plans(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_production_plan,
)
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
first_warehouse = "_Test Warehouse - _TC"
second_warehouse = "_Test Warehouse 1 - _TC"
plans = []
for warehouse in (first_warehouse, second_warehouse):
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=warehouse)
plans.append(
create_production_plan(item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1)
)
submit_work_order_from_plan(plans[1], 10, first_warehouse)
self.assertEqual(get_reserved_qty_for_production_plan(rm_item, first_warehouse), 10)
self.assertEqual(get_reserved_qty_for_production_plan(rm_item, second_warehouse), 0)
def test_plan_reservation_kept_for_work_order_without_source_warehouse(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_production_plan,
)
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
plan_warehouse = "_Test Warehouse - _TC"
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=plan_warehouse)
plan = create_production_plan(item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1)
submit_work_order_from_plan(plan, 5, None)
self.assertEqual(get_reserved_qty_for_production_plan(rm_item, plan_warehouse), 10)
self.assertEqual(
frappe.db.get_value(
"Bin", {"item_code": rm_item, "warehouse": plan_warehouse}, "reserved_qty_for_production_plan"
),
10,
)
def test_plan_reservation_released_when_plan_completes(self):
plan, work_order = make_plan_with_sub_assembly()
work_order.submit()
make_stock_entry(
item_code=plan.sub_assembly_items[0].production_item,
qty=5,
rate=10,
target=work_order.source_warehouse,
)
frappe.get_doc(make_se_from_wo(work_order.name, "Material Transfer for Manufacture", 5)).submit()
raw_material = plan.mr_items[0]
bin = frappe.get_doc(
"Bin", {"item_code": raw_material.item_code, "warehouse": raw_material.warehouse}
)
self.assertEqual(bin.reserved_qty_for_production_plan, 5)
manufacture = frappe.get_doc(make_se_from_wo(work_order.name, "Manufacture", 5))
manufacture.submit()
self.assertEqual(frappe.db.get_value("Production Plan", plan.name, "status"), "Completed")
bin.reload()
self.assertEqual(bin.reserved_qty_for_production_plan, 0)
manufacture.cancel()
bin.reload()
self.assertEqual(bin.reserved_qty_for_production_plan, 5)
def test_plan_reservation_released_when_last_work_order_is_closed(self):
from erpnext.manufacturing.doctype.work_order.work_order import close_work_order
plan, work_order = make_plan_with_sub_assembly()
work_order.submit()
plan.make_work_order()
sub_assembly = plan.sub_assembly_items[0]
sub_assembly_work_order = frappe.get_doc(
"Work Order", {"production_plan": plan.name, "production_item": sub_assembly.production_item}
)
sub_assembly_work_order.wip_warehouse = "_Test Warehouse 2 - _TC"
sub_assembly_work_order.submit()
make_stock_entry(
item_code=sub_assembly.production_item, qty=5, rate=10, target=work_order.source_warehouse
)
frappe.get_doc(make_se_from_wo(work_order.name, "Material Transfer for Manufacture", 5)).submit()
frappe.get_doc(make_se_from_wo(work_order.name, "Manufacture", 5)).submit()
bin = frappe.get_doc(
"Bin", {"item_code": sub_assembly.production_item, "warehouse": sub_assembly.fg_warehouse}
)
self.assertEqual(bin.reserved_qty_for_production_plan, 5)
close_work_order(sub_assembly_work_order.name, "Closed")
self.assertEqual(frappe.db.get_value("Production Plan", plan.name, "status"), "Completed")
bin.reload()
self.assertEqual(bin.reserved_qty_for_production_plan, 0)
def test_closed_plan_stays_closed_on_production(self):
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
warehouse = "_Test Warehouse - _TC"
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=warehouse)
plan = create_production_plan(item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1)
work_order = submit_work_order_from_plan(plan, 5, warehouse)
plan.set_status(close=True)
make_stock_entry(item_code=rm_item, qty=5, rate=10, target=warehouse)
frappe.get_doc(make_se_from_wo(work_order.name, "Material Transfer for Manufacture", 5)).submit()
frappe.get_doc(make_se_from_wo(work_order.name, "Manufacture", 5)).submit()
self.assertEqual(frappe.db.get_value("Production Plan", plan.name, "status"), "Closed")
self.assertEqual(
frappe.db.get_value(
"Bin", {"item_code": rm_item, "warehouse": warehouse}, "reserved_qty_for_production_plan"
),
0,
)
def test_plan_reservation_offsets_are_distributed_across_warehouses(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
_get_remaining_reserved_qty,
)
reservations = {"Warehouse A": 6, "Warehouse B": 4}
cases = [
({"Warehouse A": 5}, 1, 4),
({"Warehouse C": 5}, 3, 2),
({"Warehouse A": 8}, 0, 2),
({"Warehouse C": 20}, 0, 0),
]
for work_order_reservations, warehouse_a_qty, warehouse_b_qty in cases:
with self.subTest(work_order_reservations=work_order_reservations):
self.assertEqual(
_get_remaining_reserved_qty(reservations, work_order_reservations, "Warehouse A"),
warehouse_a_qty,
)
self.assertEqual(
_get_remaining_reserved_qty(reservations, work_order_reservations, "Warehouse B"),
warehouse_b_qty,
)
self.assertEqual(
_get_remaining_reserved_qty({"Warehouse A": 5}, {"Warehouse B": 4}, "Warehouse A"), 1
)
def test_reserved_qty_for_production_plan_for_less_rm_qty(self):
from erpnext.stock.utils import get_or_make_bin
@@ -4146,51 +3972,6 @@ def create_production_plan(**args):
return pln
def submit_work_order_from_plan(plan, qty, source_warehouse):
production_item = next(iter(plan.get_production_items().values()))
production_item["qty"] = qty
work_order = frappe.get_doc("Work Order", plan.create_work_order(production_item))
work_order.source_warehouse = source_warehouse
work_order.wip_warehouse = "_Test Warehouse 2 - _TC"
work_order.fg_warehouse = "_Test Warehouse - _TC"
for item in work_order.required_items:
item.source_warehouse = source_warehouse
work_order.submit()
return work_order
def make_plan_with_sub_assembly():
warehouse = "_Test Warehouse - _TC"
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
sub_assembly_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
fg_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
make_bom(item=sub_assembly_item, raw_materials=[rm_item], source_warehouse=warehouse)
make_bom(item=fg_item, raw_materials=[sub_assembly_item], source_warehouse=warehouse)
plan = create_production_plan(
item_code=fg_item,
planned_qty=5,
ignore_existing_ordered_qty=1,
sub_assembly_warehouse="_Test Warehouse 1 - _TC",
skip_getting_mr_items=1,
do_not_submit=1,
)
plan.get_sub_assembly_items()
for row in get_items_for_material_requests(plan.as_dict()):
plan.append("mr_items", row)
plan.submit()
production_item = next(iter(plan.get_production_items().values()))
production_item["use_multi_level_bom"] = 0
work_order = frappe.get_doc("Work Order", plan.create_work_order(production_item))
work_order.source_warehouse = warehouse
work_order.wip_warehouse = "_Test Warehouse 2 - _TC"
work_order.fg_warehouse = warehouse
for item in work_order.required_items:
item.source_warehouse = warehouse
return plan, work_order
def make_bom(**args):
args = frappe._dict(args)

View File

@@ -3859,126 +3859,6 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, transfer_entry.submit)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"enable_stock_reservation": 1, "auto_reserve_serial_and_batch": 1},
)
def test_transfer_of_other_batch_keeps_reservation_open(self):
production_item = "Test Other Batch Release FG"
rm_item = "Test Other Batch Release RM"
source_warehouse = "Stores - _TC"
make_item(production_item, {"is_stock_item": 1})
make_item(
rm_item,
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "TST-BATCH-OTH-.###",
"create_new_batch": 1,
},
)
make_bom(item=production_item, source_warehouse=source_warehouse, raw_materials=[rm_item])
batches = []
for _ in range(2):
receipt = test_stock_entry.make_stock_entry(
item_code=rm_item, target=source_warehouse, qty=50, basic_rate=100
)
batches.append(get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle))
wo = make_wo_order_test_record(
item=production_item, qty=50, reserve_stock=1, source_warehouse=source_warehouse
)
sre = frappe.get_doc(
"Stock Reservation Entry",
{"voucher_no": wo.name, "warehouse": source_warehouse, "docstatus": 1},
)
reserved_batch = sre.sb_entries[0].batch_no
other_batch = batches[1] if batches[0] == reserved_batch else batches[0]
transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 50))
for row in transfer.items:
row.update(
{"batch_no": other_batch, "use_serial_batch_fields": 1, "serial_and_batch_bundle": None}
)
transfer.insert()
transfer.submit()
sre.reload()
self.assertEqual(sre.status, "Reserved")
self.assertEqual(sre.transferred_qty, 0)
self.assertEqual([(row.batch_no, row.delivered_qty) for row in sre.sb_entries], [(reserved_batch, 0)])
frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 50)).submit()
wo.reload()
self.assertEqual(wo.required_items[0].stock_reserved_qty, 50)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"enable_stock_reservation": 1, "auto_reserve_serial_and_batch": 1},
)
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 1})
def test_material_consumption_uses_batch_reservation(self):
production_item = "Test Consumption Reservation FG"
rm_item = "Test Consumption Reservation RM"
source_warehouse = "Stores - _TC"
make_item(production_item, {"is_stock_item": 1})
make_item(
rm_item,
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "TST-BATCH-MCM-.###",
"create_new_batch": 1,
},
)
make_bom(item=production_item, source_warehouse=source_warehouse, raw_materials=[rm_item])
test_stock_entry.make_stock_entry(item_code=rm_item, target=source_warehouse, qty=50, basic_rate=100)
wo = make_wo_order_test_record(
item=production_item, qty=50, reserve_stock=1, source_warehouse=source_warehouse
)
frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 50)).submit()
frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 50)).submit()
wip_reservation = frappe.db.get_value(
"Stock Reservation Entry",
{"voucher_no": wo.name, "warehouse": wo.wip_warehouse, "docstatus": 1},
["consumed_qty", "status"],
as_dict=True,
)
self.assertEqual(wip_reservation.consumed_qty, 50)
self.assertEqual(wip_reservation.status, "Delivered")
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"enable_stock_reservation": 1, "allow_negative_stock": 0},
)
def test_ledger_preview_ignores_own_work_order_reservation(self):
from erpnext.controllers.stock_controller import get_stock_ledger_preview
production_item = "Test Preview Reservation FG"
rm_item = "Test Preview Reservation RM"
source_warehouse = "Stores - _TC"
make_item(production_item, {"is_stock_item": 1})
make_item(rm_item, {"is_stock_item": 1})
make_bom(item=production_item, source_warehouse=source_warehouse, raw_materials=[rm_item])
test_stock_entry.make_stock_entry(item_code=rm_item, target=source_warehouse, qty=20, basic_rate=100)
wo = make_wo_order_test_record(
item=production_item, qty=20, reserve_stock=1, source_warehouse=source_warehouse
)
transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 20))
transfer.insert()
transfer.run_method("before_sl_preview")
_, sl_data = get_stock_ledger_preview(transfer, frappe._dict(company=transfer.company))
self.assertEqual(len(sl_data), 2)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"enable_stock_reservation": 1, "allow_partial_reservation": 1},

View File

@@ -788,6 +788,8 @@ erpnext.work_order = {
);
}
erpnext.work_order.setup_stock_reservation(frm);
if (!frm.doc.track_semi_finished_goods) {
const show_start_btn =
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
@@ -931,14 +933,11 @@ erpnext.work_order = {
}
}
}
erpnext.work_order.setup_stock_reservation(frm);
},
setup_stock_reservation(frm) {
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
if (
!["Closed", "Completed"].includes(frm.doc.status) &&
frm.events.has_unreserved_stock(frm) &&
(frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty)
) {
@@ -950,11 +949,13 @@ erpnext.work_order = {
}
if (frm.events.has_reserved_stock(frm)) {
frm.add_custom_button(
__("Unreserve"),
() => erpnext.stock_reservation.unreserve_stock(frm),
__("Stock Reservation")
);
if (frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty) {
frm.add_custom_button(
__("Unreserve"),
() => erpnext.stock_reservation.unreserve_stock(frm),
__("Stock Reservation")
);
}
frm.add_custom_button(
__("Reserved Stock"),

View File

@@ -46,8 +46,6 @@ from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
from erpnext.utilities.transaction_base import validate_uom_is_integer
CONSUMPTION_PURPOSES = ("Manufacture", "Material Consumption for Manufacture")
class OverProductionError(frappe.ValidationError):
pass
@@ -263,9 +261,6 @@ class WorkOrder(Document):
def on_discard(self):
self.db_set("status", "Cancelled")
def before_insert(self):
self.enable_reserve_stock_for_produced_serial_no()
def validate(self):
self.validate_production_item()
if self.bom_no:
@@ -333,20 +328,6 @@ class WorkOrder(Document):
title=_("Target Warehouse Reservation Error"),
)
def enable_reserve_stock_for_produced_serial_no(self):
"""Reserve the produced serial nos for a Sales Order Item with ensure delivery by serial no."""
if self.reserve_stock or not self.sales_order_item:
return
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
if frappe.db.get_value(
"Sales Order Item", self.sales_order_item, "ensure_delivery_based_on_produced_serial_no"
):
self.reserve_stock = 1
def set_reserve_stock(self):
for row in self.required_items:
row.reserve_stock = self.reserve_stock
@@ -1402,8 +1383,8 @@ class WorkOrder(Document):
doc = frappe.get_doc("Production Plan", self.production_plan)
doc.flags.ignore_permissions = True
doc.update_status_and_bin_qty()
doc.update_raw_material_bin_qty({d.item_code for d in self.required_items})
doc.set_status()
doc.db_set("status", doc.status)
def update_work_order_qty_in_so(self):
if (not self.sales_order and not self.sales_order_item) or self.production_plan_sub_assembly_item:
@@ -1922,17 +1903,15 @@ class WorkOrder(Document):
if qty_to_update < 0:
continue
doc.db_set("transferred_qty", flt(qty_to_update), update_modified=False)
if (doc.has_batch_no or doc.has_serial_no) and doc.reservation_based_on == "Serial and Batch":
doc.consume_serial_batch_for_material_transfer(row_wise_serial_batch)
qty_to_update = doc.matched_serial_batch_qty
doc.db_set("transferred_qty", flt(qty_to_update), update_modified=False)
if doc.transferred_qty >= doc.reserved_qty:
doc.db_set("status", "Closed", update_modified=False)
doc.update_status()
doc.update_reserved_stock_in_bin()
doc.update_reserved_qty_in_voucher()
def update_returned_qty(self):
returned_dict = self._material_transfer_qty_by_item(is_return=1)
@@ -1971,7 +1950,7 @@ class WorkOrder(Document):
if not self.skip_transfer:
filters["from_voucher_no"] = ("is", "set")
row_wise_serial_batch = get_row_wise_serial_batch(self.name, CONSUMPTION_PURPOSES)
row_wise_serial_batch = get_row_wise_serial_batch(self.name, "Manufacture")
if names := frappe.get_all(
"Stock Reservation Entry", filters=filters, pluck="name", order_by="creation"
@@ -1989,11 +1968,9 @@ class WorkOrder(Document):
if (doc.has_batch_no or doc.has_serial_no) and doc.reservation_based_on == "Serial and Batch":
doc.consume_serial_batch_for_material_transfer(row_wise_serial_batch)
doc.db_set("consumed_qty", doc.matched_serial_batch_qty, update_modified=False)
doc.update_status()
doc.update_reserved_stock_in_bin()
doc.update_reserved_qty_in_voucher()
def validate_reserved_qty(self):
sre_details = get_sre_details(self.name)
@@ -2550,7 +2527,7 @@ def get_consumed_qty(work_order, item_code):
.select(fn.Sum(stock_entry_detail.transfer_qty).as_("qty"))
.where(
(stock_entry.work_order == work_order)
& (stock_entry.purpose.isin(CONSUMPTION_PURPOSES))
& (stock_entry.purpose.isin(["Manufacture", "Material Consumption for Manufacture"]))
& (stock_entry.docstatus == 1)
& (stock_entry_detail.s_warehouse.isnotnull())
& ((stock_entry_detail.item_code == item_code) | (stock_entry_detail.original_item == item_code))
@@ -3236,12 +3213,11 @@ def get_row_wise_serial_batch(work_order, purpose=None):
if not purpose:
purpose = "Material Transfer for Manufacture"
purposes = [purpose] if isinstance(purpose, str) else purpose
stock_entries = frappe.get_all(
"Stock Entry",
filters={
"work_order": work_order,
"purpose": ("in", purposes),
"purpose": purpose,
"docstatus": 1,
},
pluck="name",

View File

@@ -1,49 +1,4 @@
frappe.provide("erpnext.accounts.bank_reconciliation");
frappe.provide("erpnext.accounts.bank_reconciliation.voucher_types");
// other apps can register more "Create Voucher" types: { get_fields(dm), is_applicable(bank_transaction), create(dm, values, allow_edit) }
erpnext.accounts.bank_reconciliation.voucher_types = {
"Payment Entry": {
create(dialog_manager, values, allow_edit) {
return frappe.xcall(
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
{
bank_transaction_name: dialog_manager.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
project: values.project,
cost_center: values.cost_center,
allow_edit: allow_edit,
company_bank_account: values?.bank_account || dialog_manager?.bank_account,
}
);
},
},
"Journal Entry": {
create(dialog_manager, values, allow_edit) {
return frappe.xcall(
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
{
bank_transaction_name: dialog_manager.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
entry_type: values.journal_entry_type,
second_account: values.second_account,
allow_edit: allow_edit,
}
);
},
},
...erpnext.accounts.bank_reconciliation.voucher_types,
};
erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
constructor(
@@ -94,7 +49,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
this.bank_transaction = r.message;
r.message.payment_entry = 1;
r.message.journal_entry = 1;
this.set_document_type_options();
this.dialog.set_values(r.message);
this.copy_data_to_voucher();
this.dialog.show();
@@ -103,29 +57,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
});
}
get_voucher_types() {
return erpnext.accounts.bank_reconciliation.voucher_types;
}
get_document_types() {
return Object.entries(this.get_voucher_types())
.filter(
([, voucher_type]) =>
!this.bank_transaction ||
!voucher_type.is_applicable ||
voucher_type.is_applicable(this.bank_transaction)
)
.map(([document_type]) => document_type);
}
set_document_type_options() {
const document_types = this.get_document_types();
this.dialog.set_df_property("document_type", "options", document_types.join("\n"));
if (!document_types.includes(this.dialog.get_value("document_type"))) {
this.dialog.set_value("document_type", document_types[0]);
}
}
copy_data_to_voucher() {
let copied = {
reference_number: this.bank_transaction.reference_number || this.bank_transaction.description,
@@ -255,7 +186,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
label: __("Document Type"),
fieldname: "document_type",
fieldtype: "Select",
options: this.get_document_types().join("\n"),
options: `Payment Entry\nJournal Entry`,
default: "Payment Entry",
depends_on: "eval:doc.action=='Create Voucher'",
},
@@ -467,7 +398,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
};
},
},
...this.get_additional_voucher_fields(),
{
fieldtype: "Section Break",
fieldname: "details_section",
@@ -529,12 +459,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
];
}
get_additional_voucher_fields() {
return Object.values(this.get_voucher_types()).flatMap((voucher_type) =>
voucher_type.get_fields ? voucher_type.get_fields(this) : []
);
}
get_selected_attributes() {
let selected_attributes = [];
this.dialog.$wrapper.find(".checkbox input").each((i, col) => {
@@ -553,7 +477,10 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
reconciliation_dialog_primary_action(values) {
if (values.action == "Match Against Voucher") this.match(values);
else if (values.action == "Create Voucher") this.create_voucher(values);
if (values.action == "Create Voucher" && values.document_type == "Payment Entry")
this.add_payment_entry(values);
if (values.action == "Create Voucher" && values.document_type == "Journal Entry")
this.add_journal_entry(values);
else if (values.action == "Update Bank Transaction") this.update_transaction(values);
}
@@ -586,25 +513,54 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
});
}
create_voucher(values, allow_edit = false) {
const voucher_type = this.get_voucher_types()[values.document_type];
if (!voucher_type) {
frappe.throw(__("Cannot create {0} from a Bank Transaction", [values.document_type]));
}
add_payment_entry(values) {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
args: {
bank_transaction_name: this.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
project: values.project,
cost_center: values.cost_center,
company_bank_account: values?.bank_account || this?.bank_account,
},
callback: (response) => {
const alert_string = __("Bank Transaction {0} added as Payment Entry", [
this.bank_transaction.name,
]);
frappe.show_alert(alert_string);
this.update_dt_cards(response.message);
this.dialog.hide();
},
});
}
return voucher_type.create(this, values, allow_edit).then((message) => {
if (allow_edit) {
const doc = frappe.model.sync(message);
track_voucher(doc[0].doctype, doc[0].name, this.bank_transaction.name);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
return;
}
frappe.show_alert(
__("Bank Transaction {0} added as {1}", [this.bank_transaction.name, values.document_type])
);
this.update_dt_cards(message);
this.dialog.hide();
add_journal_entry(values) {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
args: {
bank_transaction_name: this.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
entry_type: values.journal_entry_type,
second_account: values.second_account,
},
callback: (response) => {
const alert_string = __("Bank Transaction {0} added as Journal Entry", [
this.bank_transaction.name,
]);
frappe.show_alert(alert_string);
this.update_dt_cards(response.message);
this.dialog.hide();
},
});
}
@@ -628,7 +584,50 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
edit_in_full_page() {
const values = this.dialog.get_values(true);
return this.create_voucher(values, true);
if (values.document_type == "Payment Entry") {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
args: {
bank_transaction_name: this.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
project: values.project,
cost_center: values.cost_center,
allow_edit: true,
company_bank_account: values?.bank_account || this?.bank_account,
},
callback: (r) => {
const doc = frappe.model.sync(r.message);
track_voucher(doc[0].doctype, doc[0].name, this.bank_transaction.name);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
},
});
} else {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
args: {
bank_transaction_name: this.bank_transaction.name,
reference_number: values.reference_number,
reference_date: values.reference_date,
party_type: values.party_type,
party: values.party,
posting_date: values.posting_date,
mode_of_payment: values.mode_of_payment,
entry_type: values.journal_entry_type,
second_account: values.second_account,
allow_edit: true,
},
callback: (r) => {
var doc = frappe.model.sync(r.message);
track_voucher(doc[0].doctype, doc[0].name, this.bank_transaction.name);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
},
});
}
}
};
@@ -640,7 +639,7 @@ const track_voucher = (doctype, docname, bank_transaction_name) => {
pending_reconciliations.set(voucher_key(doctype, docname), bank_transaction_name);
};
for (const voucher_doctype of Object.keys(erpnext.accounts.bank_reconciliation.voucher_types)) {
for (const voucher_doctype of ["Payment Entry", "Journal Entry"]) {
frappe.ui.form.on(voucher_doctype, {
before_save(frm) {
frm.__pending_reconciliation_key = voucher_key(frm.doctype, frm.doc.name);

View File

@@ -216,11 +216,8 @@ $.extend(erpnext.stock_reservation, {
unreserve_stock(frm) {
erpnext.stock_reservation.get_stock_reservation_entries(frm.doctype, frm.docname).then((r) => {
if (!r.exc && r.message) {
const sre_entries = r.message.filter(
(sre) => erpnext.stock_reservation.get_held_qty(sre) > 0
);
if (sre_entries.length > 0) {
erpnext.stock_reservation.prepare_for_cancel_sre_entries(frm, sre_entries);
if (r.message.length > 0) {
erpnext.stock_reservation.prepare_for_cancel_sre_entries(frm, r.message);
} else {
frappe.msgprint(__("No reserved stock to unreserve."));
}
@@ -256,7 +253,7 @@ $.extend(erpnext.stock_reservation, {
sre: sre.name,
item_code: sre.item_code,
warehouse: sre.warehouse,
qty: erpnext.stock_reservation.get_held_qty(sre),
qty: flt(sre.reserved_qty) - flt(sre.delivered_qty),
});
});
@@ -264,12 +261,6 @@ $.extend(erpnext.stock_reservation, {
dialog.show();
},
get_held_qty(sre) {
return (
flt(sre.reserved_qty) - flt(sre.delivered_qty) - flt(sre.transferred_qty) - flt(sre.consumed_qty)
);
},
cancel_stock_reservation(dialog, frm) {
let data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
let method = "erpnext.manufacturing.doctype.work_order.work_order.cancel_stock_reservation_entries";

View File

@@ -6,10 +6,6 @@ erpnext.utils.CRMActivities = class CRMActivities {
refresh() {
var me = this;
$(this.open_activities_wrapper).empty();
// an unsaved doc has no activities and its temp name can't be permission-checked
if (this.frm.is_new()) return;
let cur_form_footer = this.form_wrapper.find(".form-footer");
// all activities

View File

@@ -98,106 +98,6 @@ class TestUaeVat201(ERPNextTestSuite):
self.assertEqual(get_standard_rated_expenses_total(filters), 917.5)
self.assertEqual(get_standard_rated_expenses_tax(filters), 50)
@ERPNextTestSuite.change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": True}
)
def test_uae_vat_201_sales_vat_in_foreign_currency(self):
"""VAT on a foreign currency invoice must be reported in company currency."""
si = create_sales_invoice(
company="_Test Company UAE VAT",
customer="_Test UAE Customer",
currency="USD",
conversion_rate=3.67,
rate=1000,
qty=1,
warehouse="Finished Goods - _TCUV",
debit_to="Debtors - _TCUV",
income_account="Sales - _TCUV",
expense_account="Cost of Goods Sold - _TCUV",
cost_center="Main - _TCUV",
item="_Test UAE VAT Item",
do_not_save=1,
)
si.vat_emirate = "Dubai"
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TCUV",
"cost_center": "Main - _TCUV",
"description": "VAT 5% @ 5.0",
"rate": 5.0,
},
)
si.submit()
filters = {"company": "_Test Company UAE VAT"}
amounts_by_emirate = dict(
(emirate, (amount, vat)) for emirate, amount, vat in get_total_emiratewise(filters)
)
amount, vat = amounts_by_emirate["Dubai"]
self.assertEqual(amount, 3670)
self.assertEqual(vat, 183.5)
self.assertEqual(vat, si.taxes[0].base_tax_amount_after_discount_amount)
self.assertNotEqual(vat, si.items[0].tax_amount)
def test_uae_vat_201_mixed_invoice_excludes_exempt_and_zero_rated_vat(self):
si = create_sales_invoice(
company="_Test Company UAE VAT",
customer="_Test UAE Customer",
currency="AED",
rate=100,
qty=1,
warehouse="Finished Goods - _TCUV",
debit_to="Debtors - _TCUV",
income_account="Sales - _TCUV",
expense_account="Cost of Goods Sold - _TCUV",
cost_center="Main - _TCUV",
item="_Test UAE VAT Item",
do_not_save=1,
)
si.vat_emirate = "Ajman"
for item_code in ("_Test UAE VAT Zero Rated Item", "_Test UAE VAT Exempt Item"):
si.append(
"items",
{
"item_code": item_code,
"qty": 1,
"rate": 100,
"warehouse": "Finished Goods - _TCUV",
"income_account": "Sales - _TCUV",
"expense_account": "Cost of Goods Sold - _TCUV",
"cost_center": "Main - _TCUV",
},
)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TCUV",
"cost_center": "Main - _TCUV",
"description": "VAT 5% @ 5.0",
"rate": 5.0,
},
)
si.submit()
# the single On Net Total row taxes all three items, so the invoice level figure is 15
self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 15)
filters = {"company": "_Test Company UAE VAT"}
amounts_by_emirate = dict(
(emirate, (amount, vat)) for emirate, amount, vat in get_total_emiratewise(filters)
)
amount, vat = amounts_by_emirate["Ajman"]
# only the standard rated row belongs in box 1
self.assertEqual(amount, 100)
self.assertEqual(vat, 5)
self.assertEqual(get_zero_rated_total(filters), 100)
self.assertEqual(get_exempt_total(filters), 100)
def set_vat_accounts():
if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"):

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from erpnext import get_region
@@ -145,67 +144,26 @@ def append_data(data, no, legend, amount, vat_amount):
def get_total_emiratewise(filters):
"""Returns Emiratewise Amount and Taxes."""
amounts = get_emiratewise_standard_rated_amount(filters)
vat_amounts = get_emiratewise_vat_amount(filters)
return [
(emirate, amounts.get(emirate, 0), vat_amounts.get(emirate, 0))
for emirate in dict.fromkeys([*amounts, *vat_amounts])
]
def get_emiratewise_standard_rated_amount(filters):
"""Returns emiratewise net amount of standard rated supplies in company currency."""
i = frappe.qb.DocType("Sales Invoice Item")
s = frappe.qb.DocType("Sales Invoice")
query = (
frappe.qb.from_(i)
.inner_join(s)
.on(i.parent == s.name)
.select(s.vat_emirate, Sum(i.base_net_amount))
.where((s.docstatus == 1) & (i.is_exempt != 1) & (i.is_zero_rated != 1))
.groupby(s.vat_emirate)
)
for condition in get_sales_conditions(filters, s):
query = query.where(condition)
return dict(query.run())
def get_emiratewise_vat_amount(filters):
"""Returns emiratewise VAT on standard rated supplies in company currency.
Item Wise Tax Detail.amount is the item's share of the tax row already converted to
company currency, so it keeps the item level exempt / zero rated split.
"""
i = frappe.qb.DocType("Sales Invoice Item")
s = frappe.qb.DocType("Sales Invoice")
t = frappe.qb.DocType("Sales Taxes and Charges")
d = frappe.qb.DocType("Item Wise Tax Detail")
uae_vat = frappe.qb.DocType("UAE VAT Account")
query = (
frappe.qb.from_(d)
.inner_join(s)
.on(d.parent == s.name)
.inner_join(i)
.on(d.item_row == i.name)
.inner_join(t)
.on(d.tax_row == t.name)
.select(s.vat_emirate, Sum(d.amount))
.where(
(d.parenttype == "Sales Invoice")
& (s.docstatus == 1)
& (i.is_exempt != 1)
& (i.is_zero_rated != 1)
& t.account_head.isin(
frappe.qb.from_(uae_vat)
.select(uae_vat.account)
.where(uae_vat.parent == filters.get("company"))
)
conditions = get_conditions(filters)
try:
return frappe.db.sql(
f"""
select
s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount)
from
`tabSales Invoice Item` i inner join `tabSales Invoice` s
on
i.parent = s.name
where
s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1
{conditions}
group by
s.vat_emirate;
""",
filters,
)
.groupby(s.vat_emirate)
)
for condition in get_sales_conditions(filters, s):
query = query.where(condition)
return dict(query.run())
except (IndexError, TypeError):
return 0
def get_emirates():
@@ -465,15 +423,3 @@ def get_conditions(filters):
if filters.get(opts[0]):
conditions += opts[1]
return conditions
def get_sales_conditions(filters, sales_invoice):
"""Return Query Builder conditions for Sales Invoice report filters."""
conditions = []
if filters.get("company"):
conditions.append(sales_invoice.company == filters.get("company"))
if filters.get("from_date"):
conditions.append(sales_invoice.posting_date >= filters.get("from_date"))
if filters.get("to_date"):
conditions.append(sales_invoice.posting_date <= filters.get("to_date"))
return conditions

View File

@@ -158,11 +158,6 @@ frappe.ui.form.on("Sales Order", {
frm.set_df_property("reserve_stock", "read_only", 1);
frm.set_df_property("reserve_stock", "hidden", 1);
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
frm.fields_dict.items.grid.update_docfield_property(
"ensure_delivery_based_on_produced_serial_no",
"hidden",
1
);
frm.fields_dict.items.grid.update_docfield_property(
"reserve_stock",
"default",

View File

@@ -837,9 +837,6 @@ class SalesOrder(SellingController):
if item.reserve_stock and (not enable_stock_reservation or not cint(item.is_stock_item)):
item.reserve_stock = 0
if item.ensure_delivery_based_on_produced_serial_no and not enable_stock_reservation:
item.ensure_delivery_based_on_produced_serial_no = 0
def has_unreserved_stock(self) -> bool:
"""Returns True if there is any unreserved item in the Sales Order."""

View File

@@ -14,8 +14,7 @@
"disable_rounded_total",
"disable_in_words",
"use_posting_datetime_for_naming_documents",
"demo_company",
"enable_item_search_index"
"demo_company"
],
"fields": [
{
@@ -83,14 +82,6 @@
"options": "Company",
"read_only": 1
},
{
"default": "0",
"description": "Speeds up Item search on large catalogues. Requires a single app server: the index is a file on that server's disk, and several servers would each answer from a different copy. Building it takes about 20 minutes for 3 million items, and runs in the background.",
"fieldname": "enable_item_search_index",
"fieldtype": "Check",
"label": "Enable Item Search Index",
"show_description_on_click": 1
},
{
"default": "0",
"description": "When checked, the system will use the posting datetime of the document for naming the document instead of the creation datetime of the document.",

View File

@@ -476,7 +476,6 @@ class DeliveryNote(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
self.validate_produced_serial_nos_against_reservation()
self.make_gl_entries()
self.repost_future_sle_and_gle()

View File

@@ -3108,119 +3108,6 @@ class TestDeliveryNote(ERPNextTestSuite):
dn.items[0].stock_qty = 2
dn.save()
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"enable_stock_reservation": 1, "auto_create_serial_and_batch_bundle_for_outward": 1},
)
def test_delivery_restricted_to_reserved_produced_serial_nos(self):
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
self.assertEqual(frappe.db.get_value("Work Order", {"sales_order": so.name}, "reserve_stock"), 1)
frappe.db.savepoint("unreserved_serial_no")
dn = make_delivery_note(so.name)
dn.items[0].use_serial_batch_fields = 1
dn.items[0].serial_no = unreserved[0]
dn.save()
self.assertRaises(frappe.ValidationError, dn.submit)
frappe.db.rollback(save_point="unreserved_serial_no")
dn = make_delivery_note(so.name)
dn.save()
dn.submit()
self.assertEqual(get_serial_nos_from_bundle(dn.items[0].serial_and_batch_bundle), reserved)
@ERPNextTestSuite.change_settings("Stock Settings", {"enable_stock_reservation": 0})
def test_ensure_delivery_by_serial_no_cleared_without_stock_reservation(self):
item_code = make_item("Test Ensure Serial Without SRE", {"is_stock_item": 1, "has_serial_no": 1}).name
so = make_sales_order(item_code=item_code, qty=1, do_not_save=True)
so.items[0].ensure_delivery_based_on_produced_serial_no = 1
so.save()
self.assertEqual(so.items[0].ensure_delivery_based_on_produced_serial_no, 0)
@ERPNextTestSuite.change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_production_plan_work_order_reserves_stock_for_ensure_delivery_by_serial_no(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import (
create_production_plan,
make_bom,
)
fg_item = make_item(
"Test PP Produced Serial FG",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TPPSFG-.####"},
).name
rm_item = make_item("Test PP Produced Serial RM", {"is_stock_item": 1}).name
make_bom(item=fg_item, raw_materials=[rm_item])
so = make_sales_order(item_code=fg_item, qty=1, do_not_submit=True)
so.items[0].ensure_delivery_based_on_produced_serial_no = 1
so.submit()
pln = create_production_plan(
company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True
)
pln.make_work_order()
self.assertEqual(
frappe.db.get_value(
"Work Order", {"production_plan": pln.name}, ["sales_order_item", "reserve_stock"]
),
(so.items[0].name, 1),
)
def make_so_with_reserved_produced_serial_no():
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_wo_entry
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_serial_nos_for_voucher_detail_nos,
)
warehouse = "_Test Warehouse - _TC"
fg_item = make_item(
"Test Produced Serial FG", {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TPSFG-.####"}
).name
rm_item = make_item("Test Produced Serial RM", {"is_stock_item": 1}).name
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse=warehouse)
make_stock_entry(item_code=rm_item, target=warehouse, qty=1, basic_rate=100)
make_stock_entry(item_code=fg_item, target=warehouse, qty=1, basic_rate=100)
so = make_sales_order(item_code=fg_item, qty=1, warehouse=warehouse, do_not_submit=True)
so.items[0].ensure_delivery_based_on_produced_serial_no = 1
so.submit()
wo = make_wo_order_test_record(
item=fg_item,
qty=1,
sales_order=so.name,
source_warehouse=warehouse,
wip_warehouse=warehouse,
fg_warehouse=warehouse,
skip_transfer=1,
do_not_save=True,
)
wo.sales_order_item = so.items[0].name
wo.insert()
wo.submit()
frappe.get_doc(make_wo_entry(wo.name, "Manufacture", 1)).submit()
reserved = sorted(
get_sre_reserved_serial_nos_for_voucher_detail_nos("Sales Order", [so.items[0].name])[
so.items[0].name
]
)
unreserved = frappe.get_all(
"Serial No",
filters={"item_code": fg_item, "status": "Active", "name": ("not in", reserved)},
pluck="name",
)
return so, reserved, unreserved
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -1,252 +0,0 @@
import os
import re
import sqlite3
import frappe
from frappe.search.sqlite_search import SQLiteSearch, SQLiteSearchIndexMissingError
MINIMUM_TERM_LENGTH = 3
CANDIDATE_LIMIT = 25000
LIKE_WILDCARDS = r"[%_]"
BARCODE_COLUMN = "barcodes"
TOKENIZER = "trigram remove_diacritics 1"
def get_searched_fieldnames() -> list[str]:
"""Item fields that item_query matches the search term against."""
meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields()
extras = [f for f in ("item_code", "item_group", "item_name") if f not in searchfields]
db_fieldnames = {field.fieldname for field in meta.fields}
return [f for f in [*searchfields, *extras] if f in db_fieldnames]
class ItemSearch(SQLiteSearch):
"""FTS5 trigram index over Item, for substring search on large catalogues.
A host whose copy is behind drops valid Items from its candidate list, so results depend on
which host answered. Switched on per site from Global Defaults, which carries that warning.
"""
INDEX_NAME = "item_search.db"
BUILD_VOCABULARY = False
def __init__(self, db_name=None):
fieldnames = get_searched_fieldnames()
mapped = {"title": "item_code", "content": "item_name"}
plain = [f for f in dict.fromkeys(["name", *fieldnames]) if f not in mapped.values()]
self.INDEXABLE_DOCTYPES = {
"Item": {
"fields": [*plain, mapped],
"filters": {"disabled": 0, "has_variants": 0},
}
}
self.INDEX_SCHEMA = {
"tokenizer": TOKENIZER,
"text_fields": [
"title",
"content",
BARCODE_COLUMN,
*self._extra_text_fields(fieldnames),
],
}
self.indexed_fieldnames = {"name", *fieldnames}
self._barcodes = {}
super().__init__(db_name)
@staticmethod
def _extra_text_fields(fieldnames: list[str]) -> list[str]:
return [f for f in fieldnames if f not in ("item_code", "item_name")]
def index_exists(self) -> bool:
"""A table missing a searched column cannot answer for it, so it reports itself absent.
The searched fields come from the Item meta, so a site that adds one leaves an older table
short. Callers fall back and the builder replaces it. One connection: this runs on every save.
"""
if not os.path.exists(self.db_path):
return False
return set(self.schema["text_fields"]) <= self.get_indexed_columns()
def get_indexed_columns(self) -> set[str]:
"""Columns the built table carries, empty when there is no table."""
try:
connection = self._get_connection(read_only=True)
except SQLiteSearchIndexMissingError:
return set()
try:
return {row["name"] for row in connection.execute("PRAGMA table_info(search_fts)")}
except sqlite3.Error:
return set()
finally:
connection.close()
def get_documents_paginated(self, doctype, *args, **kwargs):
"""Preload the batch's barcodes: reading them per document would be one query each."""
documents = super().get_documents_paginated(doctype, *args, **kwargs)
self._barcodes = get_barcodes_by_item([document.name for document in documents])
return documents
def index_documents_by_name(self, doctype, names: list[str]):
"""Preload this batch: the catch-up skips get_documents_paginated, and the barcodes left
from the last build batch may since have moved."""
self._barcodes = get_barcodes_by_item(names)
super().index_documents_by_name(doctype, names)
def prepare_document(self, doc):
document = super().prepare_document(doc)
if document is None:
return None
document[BARCODE_COLUMN] = self.get_barcode_text(doc.name)
return document
def _process_content(self, content):
"""Store values verbatim.
item_query rechecks every candidate with LIKE against the column in MariaDB, so the
indexed text has to be what that column holds. The framework's cleaning collapses
whitespace and replaces a URL with "[link]", which would lose those rows.
"""
return "" if content is None else str(content)
def get_barcode_text(self, item_code: str) -> str:
"""Always a string: a text column left unset drops the document from the index entirely."""
if item_code in self._barcodes:
return self._barcodes[item_code]
return get_barcodes_by_item([item_code]).get(item_code, "")
def is_search_enabled(self) -> bool:
"""Off unless Global Defaults opts in: building reads every Item, which is not free."""
return bool(frappe.get_single_value("Global Defaults", "enable_item_search_index"))
def get_search_filters(self) -> dict:
return {}
def get_candidate_item_codes(self, txt: str, searched_fields: list[str]) -> list[str] | None:
"""Item codes that can match txt, a superset the caller must still recheck with LIKE.
Covers barcodes, which item_query also searches: an Item left out is filtered away even
when its barcode matches. Answers only when the index carries every field the query
searches, because a caller may pass any Item field as searchfield. `name` counts as
indexed: Item.autoname assigns it from item_code.
"""
if not self.is_search_enabled() or not self.index_exists():
return None
if not set(searched_fields) <= self.indexed_fieldnames:
return None
match_query = build_match_query(txt)
if match_query is None:
return None
names = self.run_match(match_query)
if names is None or len(names) >= CANDIDATE_LIMIT:
return None
return names
def run_match(self, match_query: str) -> list[str] | None:
"""None means the index cannot answer. An empty list means it answered: nothing matches."""
connection = self._get_connection(read_only=True)
try:
matched = connection.execute(
"SELECT name FROM search_fts WHERE search_fts MATCH ? LIMIT ?",
(match_query, CANDIDATE_LIMIT),
).fetchall()
except sqlite3.Error:
frappe.log_error("Item search index lookup failed")
return None
finally:
connection.close()
return [row["name"] for row in matched]
def build_match_query(txt: str) -> str | None:
"""FTS5 query matching a superset of LIKE %txt%, or None when it cannot narrow the scan."""
if "\\" in txt:
return None
fragments = [fragment.strip() for fragment in re.split(LIKE_WILDCARDS, txt)]
usable = [fragment for fragment in fragments if len(fragment) >= MINIMUM_TERM_LENGTH]
if not usable:
return None
return " AND ".join(quote_fragment(fragment) for fragment in usable)
def quote_fragment(fragment: str) -> str:
escaped = fragment.replace('"', '""')
return f'"{escaped}"'
def get_barcodes_by_item(item_codes: list[str]) -> dict[str, str]:
"""Barcodes of each item, joined into the one string the index column holds."""
if not item_codes:
return {}
rows = frappe.get_all(
"Item Barcode",
filters={"parent": ("in", item_codes), "parentfield": BARCODE_COLUMN},
fields=["parent", "barcode"],
)
barcodes = {}
for row in rows:
barcodes[row.parent] = f"{barcodes.get(row.parent, '')} {row.barcode}".strip()
return barcodes
def reindex_item(doc, method=None):
"""Queue an Item on every save.
Item Barcode rows raise no document events, so the Item save is the only signal one moved, and
no indexed field of the Item need have changed.
"""
queue_item(doc.name)
def reindex_renamed_item(doc, method=None, old=None, new=None, merge=False):
"""A rename writes the new name straight to the table without saving the Item, so on_update
never runs and the index would keep answering with the name that is gone."""
queue_item(doc.name, drop=old)
def queue_item(item_code: str, drop: str | None = None):
"""Index one Item, and drop the name a rename replaced.
Indexes before dropping, so a failure leaves the replaced name in the index rather than losing
both: a name nobody holds is an extra candidate the query filters out, a missing one hides a row.
A failed write must not fail the Item save, and must not leave the index answering either.
Nothing would record this Item as stale, and a candidate list missing it hides rows the scan
returns, so the index goes and every caller falls back until the scheduler rebuilds it.
"""
search = ItemSearch()
if not (search.is_search_enabled() and search.index_exists()):
return
try:
search.index_doc("Item", item_code)
if drop:
search.remove_doc("Item", drop)
except Exception:
frappe.log_error("Item search index update failed, dropping the index")
try:
search.drop_index()
except Exception:
frappe.log_error("Item search index could not be dropped")
def get_item_search_candidates(txt: str, searched_fields: list[str]) -> list[str] | None:
try:
return ItemSearch().get_candidate_item_codes(txt, searched_fields)
except Exception:
frappe.log_error("Item search index unavailable")
return None

View File

@@ -1,332 +0,0 @@
import sqlite3
from unittest.mock import MagicMock, patch
import frappe
from frappe.search.sqlite_search import get_search_classes, update_doc_index
from erpnext.controllers import queries
from erpnext.stock.doctype.item.item_search import ItemSearch, build_match_query
from erpnext.tests.utils import ERPNextTestSuite
class TestBuildMatchQuery(ERPNextTestSuite):
def test_quotes_the_term(self):
self.assertEqual(build_match_query("widget"), '"widget"')
def test_escapes_embedded_quotes(self):
self.assertEqual(build_match_query('say "hi"'), '"say ""hi"""')
def test_skips_terms_shorter_than_a_trigram(self):
for txt in ("", "a", "ab", " b "):
self.assertIsNone(build_match_query(txt), txt)
def test_splits_on_like_wildcards(self):
"""A wildcard splits the term, the fragments narrow, and the caller rechecks with LIKE."""
self.assertEqual(build_match_query("RAW_MAT_000123"), '"RAW" AND "MAT" AND "000123"')
self.assertEqual(build_match_query("abc%def"), '"abc" AND "def"')
def test_skips_terms_with_no_usable_fragment(self):
for txt in ("ab%cd", "ab_cd", "a%b%c"):
self.assertIsNone(build_match_query(txt), txt)
def test_skips_escaped_terms(self):
"""Backslash escapes the next LIKE wildcard, so the split would be wrong."""
self.assertIsNone(build_match_query("ab\\_cd"))
class TestItemSearchSetting(ERPNextTestSuite):
def test_the_global_defaults_checkbox_drives_the_index(self):
with self.change_settings("Global Defaults", enable_item_search_index=0):
self.assertFalse(ItemSearch().is_search_enabled())
with self.change_settings("Global Defaults", enable_item_search_index=1):
self.assertTrue(ItemSearch().is_search_enabled())
class TestItemSearchIndex(ERPNextTestSuite):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.enabled = patch.object(ItemSearch, "is_search_enabled", return_value=True)
cls.enabled.start()
cls.search = ItemSearch()
if cls.search.index_exists():
cls.search.drop_index()
cls.search.build_index()
@classmethod
def tearDownClass(cls):
cls.search.drop_index()
cls.enabled.stop()
super().tearDownClass()
def test_item_search_is_registered_for_the_lifecycle(self):
"""Without the sqlite_search hook nothing syncs the index and it silently rots."""
self.assertIn(ItemSearch, get_search_classes())
def test_a_barcode_added_after_the_build_is_searchable(self):
"""A barcode edit changes no Item field, so the Item save is the only signal there is."""
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-BARCODE-PROBE",
"item_name": "Barcode Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
self.assertIn("barcodes", self.search.schema["text_fields"])
item.append("barcodes", {"barcode": "8809988776655"})
item.save()
self.assertIn("ZZ-BARCODE-PROBE", self.candidates("8809988776655"))
self.assertEqual(self.run_query("8809988776655", None), self.run_query("8809988776655", None, False))
def test_a_new_item_is_searchable_immediately(self):
"""index_doc writes to search_fts during the save, so there is no window to miss."""
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-QUEUE-PROBE-4471",
"item_name": "Queue Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
update_doc_index(item)
self.assertIn("ZZ-QUEUE-PROBE-4471", self.candidates("4471"))
self.assertEqual(self.run_query("4471", None), self.run_query("4471", None, False))
def test_vocabulary_is_not_built(self):
"""item_query matches search_fts directly and never asks for a spelling correction."""
self.assertFalse(ItemSearch.BUILD_VOCABULARY)
connection = self.search._get_connection(read_only=True)
try:
self.assertEqual(connection.execute("SELECT count(*) FROM search_vocabulary").fetchone()[0], 0)
self.assertEqual(connection.execute("SELECT count(*) FROM search_trigrams").fetchone()[0], 0)
finally:
connection.close()
def test_tokenizer_folds_accents(self):
"""MariaDB's utf8mb4_unicode_ci LIKE is accent insensitive, so the index must be too."""
self.assertEqual(self.search.schema["tokenizer"], "trigram remove_diacritics 1")
def test_an_accented_item_is_found_by_an_unaccented_term(self):
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-CAFÉ-7781",
"item_name": "Café Filter",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
update_doc_index(item)
self.assertIn("ZZ-CAFÉ-7781", self.candidates("CAFE-7781"))
self.assertEqual(self.run_query("CAFE-7781", None), self.run_query("CAFE-7781", None, False))
def test_a_broken_index_falls_back_to_the_scan(self):
"""An unreadable index must not answer 'nothing matches' and hide every row."""
broken = MagicMock()
broken.execute.side_effect = sqlite3.DatabaseError("database disk image is malformed")
with patch.object(ItemSearch, "_get_connection", return_value=broken):
self.assertIsNone(self.candidates("Test"))
def drifted_search(self) -> ItemSearch:
extra = [*self.search.schema["text_fields"], "a_new_custom_search_field"]
class DriftedItemSearch(ItemSearch):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.schema["text_fields"] = extra
return DriftedItemSearch()
def test_an_item_without_barcodes_is_still_indexed(self):
"""A text column left unset drops the document from the index entirely."""
frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-NO-BARCODE-3312",
"item_name": "No Barcode Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
self.assertIn("ZZ-NO-BARCODE-3312", self.candidates("3312"))
def test_a_drifted_schema_reports_the_index_as_absent(self):
"""A site that adds an Item search field leaves the built table a column short."""
self.assertTrue(self.search.index_exists())
self.assertFalse(self.drifted_search().index_exists())
def test_a_search_field_outside_the_index_is_refused(self):
"""A caller may pass any Item field as searchfield, and the index carries only some."""
outside = "stock_uom"
self.assertNotIn(outside, self.search.indexed_fieldnames)
self.assertIsNone(self.candidates("Test", ["name", outside]))
self.assertIsNotNone(self.candidates("Test", ["name", "item_code"]))
def test_item_query_with_an_unindexed_searchfield_matches_the_scan(self):
"""Narrowing on a field the index does not carry would drop rows the scan returns."""
self.assertEqual(
self.run_query("Test", None, searchfield="stock_uom"),
self.run_query("Test", None, False, searchfield="stock_uom"),
)
def test_a_renamed_item_is_searchable_under_the_new_name(self):
"""A rename writes the new name without saving the Item, so on_update never fires."""
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-RENAME-FROM-5521",
"item_name": "Rename Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
self.assertIn("ZZ-RENAME-FROM-5521", self.candidates("5521"))
frappe.rename_doc("Item", item.name, "ZZ-RENAME-TO-5521", force=True)
self.assertIn("ZZ-RENAME-TO-5521", self.candidates("5521"))
self.assertNotIn("ZZ-RENAME-FROM-5521", self.candidates("5521"))
def test_repeated_spaces_and_urls_match_the_scan(self):
"""Values are indexed verbatim: cleaning them would lose rows the LIKE still matches."""
frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-VERBATIM-6612",
"item_name": "Valve 3 MM see https://example.com/spec",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
for txt in ("Valve 3", "example.com"):
with self.subTest(txt=txt):
self.assertEqual(self.run_query(txt, None), self.run_query(txt, None, False))
def test_an_index_write_failure_saves_the_item_and_drops_the_index(self):
"""The save must survive, and the index must stop answering: nothing recorded the Item as
stale, so its candidate list would hide a row the scan returns."""
with (
patch.object(ItemSearch, "index_doc", side_effect=sqlite3.OperationalError("disk I/O error")),
patch.object(frappe, "log_error") as logged,
):
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": "ZZ-WRITE-FAILURE-3390",
"item_name": "Write Failure Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
self.assertTrue(frappe.db.exists("Item", item.name))
logged.assert_called()
self.assertFalse(self.search.index_exists())
self.assertIsNone(self.candidates("3390"), "a dropped index must force the scan")
self.search.build_index()
def test_candidates_are_a_superset_of_the_scan(self):
"""The query re-filters, so extra candidates are safe but missing ones are not.
None is an answer too: the index declined, and the query scans without narrowing, which
cannot lose a row. Any term can take that path on a large enough catalogue, so the
property is anchored on one selective enough that it never can.
"""
probe = "ZZ-SUPERSET-PROBE-7413"
frappe.get_doc(
{
"doctype": "Item",
"item_code": probe,
"item_name": "Superset Probe",
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}, "name"),
"stock_uom": frappe.db.get_value("UOM", {}, "name"),
}
).insert()
self.assertIsNotNone(self.candidates(probe), "a term this selective cannot reach the limit")
for txt in (probe, "Test", "Item", "est", "_Test"):
candidates = self.candidates(txt)
if candidates is None:
continue
matched = frappe.get_all(
"Item",
filters={"name": ("like", f"%{txt}%"), "disabled": 0, "has_variants": 0},
pluck="name",
)
self.assertTrue(set(matched) <= set(candidates), txt)
def test_item_query_output_is_unchanged(self):
cases = [
("Test", None),
("Item", None),
("EST", None),
("Test", {"is_stock_item": 1}),
("_Test", None),
("ab", None),
("%est", None),
("", None),
]
for txt, filters in cases:
with self.subTest(txt=txt, filters=filters):
self.assertEqual(self.run_query(txt, filters), self.run_query(txt, filters, False))
def test_underscore_in_the_term_still_narrows(self):
"""_ is a LIKE wildcard, but the fragments around it are still indexable."""
candidates = self.candidates("_Test Item")
self.assertIsNotNone(candidates)
matched = frappe.get_all(
"Item",
filters={"name": ("like", "%_Test Item%"), "disabled": 0, "has_variants": 0},
pluck="name",
)
self.assertTrue(set(matched) <= set(candidates))
def test_no_candidates_returns_no_rows(self):
"""An empty candidate list must not reach the query, IN () is a syntax error."""
with patch.object(queries, "get_item_search_candidates", return_value=[]):
self.assertEqual(queries.item_query("Item", "Test", "name", 0, 20, None), ())
self.assertEqual(queries.item_query("Item", "Test", "name", 0, 20, None, as_dict=True), [])
def test_empty_result_matches_the_scan_shape(self):
"""The early return must give back what the query itself would, tuple or list."""
for as_dict in (False, True):
with self.subTest(as_dict=as_dict):
with patch.object(queries, "get_item_search_candidates", return_value=[]):
early = queries.item_query("Item", "ZZQQNOTHING", "name", 0, 20, None, as_dict=as_dict)
with patch.object(queries, "get_item_search_candidates", return_value=None):
scanned = queries.item_query("Item", "ZZQQNOTHING", "name", 0, 20, None, as_dict=as_dict)
self.assertEqual(early, scanned)
self.assertIs(type(early), type(scanned))
def test_item_query_paging_is_unchanged(self):
for start in (0, 3, 6):
with self.subTest(start=start):
indexed = self.run_query("Test", None, page_len=3, start=start)
scanned = self.run_query("Test", None, False, page_len=3, start=start)
self.assertEqual(indexed, scanned)
def candidates(self, txt, searched_fields=None):
return self.search.get_candidate_item_codes(
txt, searched_fields or ["name", "item_code", "item_name"]
)
def run_query(self, txt, filters, use_index=True, page_len=20, start=0, searchfield="name"):
if use_index:
return queries.item_query("Item", txt, searchfield, start, page_len, filters, as_dict=True)
with patch.object(queries, "get_item_search_candidates", return_value=None):
return queries.item_query("Item", txt, searchfield, start, page_len, filters, as_dict=True)

View File

@@ -2658,20 +2658,6 @@ class StockEntry(StockController, SubcontractingInwardController):
return False
def before_sl_preview(self):
self.release_work_order_reservation_for_preview()
def before_gl_preview(self):
self.release_work_order_reservation_for_preview()
def release_work_order_reservation_for_preview(self):
"""Releases the Work Order's own reservation as submit does, inside the rolled-back preview."""
if not self.is_stock_reserve_for_work_order():
return
self.db_set("docstatus", 1, update_modified=False)
frappe.get_doc("Work Order", self.work_order).update_required_items()
def update_wo_reservation_for_subcontracting(self):
# A "Send to Subcontractor" entry never keeps its `work_order` (validate clears it for this
# purpose), so the owning Work Order is derived from the Subcontracting Order / Purchase Order
@@ -3703,6 +3689,9 @@ class StockEntry(StockController, SubcontractingInwardController):
row.stock_qty -= flt(used_secondary_items.get(key))
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
if used_secondary_items.get(key):
used_secondary_items[key] -= row.stock_qty
if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")):
row.stock_qty = frappe.utils.ceil(row.stock_qty)
@@ -3716,7 +3705,7 @@ class StockEntry(StockController, SubcontractingInwardController):
StockEntry = frappe.qb.DocType("Stock Entry")
StockEntryDetail = frappe.qb.DocType("Stock Entry Detail")
query = (
data = (
frappe.qb.from_(StockEntry)
.inner_join(StockEntryDetail)
.on(StockEntryDetail.parent == StockEntry.name)
@@ -3736,11 +3725,9 @@ class StockEntry(StockController, SubcontractingInwardController):
& (StockEntry.docstatus == 1)
& (StockEntry.purpose.isin(["Repack", "Manufacture"]))
)
)
if self.job_card:
query = query.where(StockEntry.job_card == self.job_card)
).run(as_dict=1)
for row in query.run(as_dict=1):
for row in data:
used_secondary_items[get_secondary_item_key(row)] += row.qty
return used_secondary_items

View File

@@ -665,10 +665,6 @@ class StockReservationEntry(Document):
entry.db_update()
@property
def matched_serial_batch_qty(self):
return sum(min(flt(entry.delivered_qty), flt(entry.qty)) for entry in self.sb_entries)
def validate_stock_reservation_settings(voucher: object) -> None:
"""Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed."""
@@ -962,33 +958,6 @@ def get_sre_reserved_serial_nos_details(
return frappe._dict(query.run())
def get_sre_reserved_serial_nos_for_voucher_detail_nos(voucher_type: str, voucher_detail_nos: list) -> dict:
"""Returns {voucher_detail_no: set of reserved Serial Nos}, including the delivered ones."""
sre = frappe.qb.DocType("Stock Reservation Entry")
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(sre)
.inner_join(sb_entry)
.on(sre.name == sb_entry.parent)
.select(sre.voucher_detail_no, sb_entry.serial_no)
.distinct()
.where(
(sre.docstatus == 1)
& (sre.voucher_type == voucher_type)
& (sre.voucher_detail_no.isin(voucher_detail_nos))
& (sre.reservation_based_on == "Serial and Batch")
& (sb_entry.serial_no.isnotnull())
)
)
reserved_serial_nos = {}
for voucher_detail_no, serial_no in query.run():
reserved_serial_nos.setdefault(voucher_detail_no, set()).add(serial_no)
return reserved_serial_nos
def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos: list | None = None) -> dict:
"""Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}"""
@@ -1897,8 +1866,6 @@ def _get_stock_reservation_entries_for_voucher(
"voucher_detail_no",
"reserved_qty",
"delivered_qty",
"transferred_qty",
"consumed_qty",
"stock_uom",
]

View File

@@ -1,411 +0,0 @@
// Unit tests for the Bank Reconciliation Tool dialog's voucher type registry.
// The desk script is evaluated in a sandbox with stubbed frappe globals; run with
// `node --test erpnext/tests/js`.
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { describe, it } from "node:test";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
// values built inside the sandbox carry that realm's prototypes; strip them before strict comparisons
const plain = (value) => JSON.parse(JSON.stringify(value));
const SOURCE = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../public/js/bank_reconciliation_tool/dialog_manager.js"
);
class Dialog {
constructor(opts) {
this.fields = opts.fields;
this.primary_action = opts.primary_action;
this.values = {};
this.df_properties = {};
this.hidden = false;
this.shown = false;
this.fields_dict = {};
}
set_df_property(fieldname, property, value) {
this.df_properties[fieldname] = {
...this.df_properties[fieldname],
[property]: value,
};
}
get_value(fieldname) {
return this.values[fieldname];
}
set_value(fieldname, value) {
this.values[fieldname] = value;
}
set_values(values) {
Object.assign(this.values, values);
}
get_values() {
return { ...this.values };
}
show() {
this.shown = true;
}
hide() {
this.hidden = true;
}
}
function load_dialog_manager({
pre_registered = {},
bank_transaction = null,
} = {}) {
const calls = {
call: [],
xcall: [],
alerts: [],
msgprints: [],
routes: [],
synced: [],
form_handlers: {},
};
const sandbox = { console };
sandbox.frappe = {
provide(namespace) {
let object = sandbox;
for (const part of namespace.split(".")) {
object[part] = object[part] || {};
object = object[part];
}
},
call(opts) {
calls.call.push(opts);
if (opts.method.endsWith("get_doctypes_for_bank_reconciliation")) {
opts.callback({ message: ["Payment Entry", "Journal Entry"] });
} else if (opts.method === "frappe.client.get_value") {
opts.callback({ message: bank_transaction });
}
},
xcall(method, args) {
calls.xcall.push({ method, args });
return Promise.resolve(sandbox.xcall_result);
},
ui: {
Dialog,
form: {
on(doctype, handlers) {
calls.form_handlers[doctype] = handlers;
},
},
},
model: {
sync(message) {
calls.synced.push(message);
return [message];
},
},
set_route: (...route) => calls.routes.push(route),
show_alert: (message) => calls.alerts.push(message),
msgprint: (message) => calls.msgprints.push(message),
throw(message) {
throw new Error(message);
},
scrub: (text) => text.toLowerCase().replace(/ /g, "_"),
boot: {
party_account_types: { Customer: "Receivable", Supplier: "Payable" },
},
};
sandbox.__ = (text, args = []) =>
text.replace(/\{(\d+)\}/g, (_, index) => args[index]);
sandbox.$ = Object.assign(() => ({}), {
each: (items, fn) => items.forEach((item, index) => fn(index, item)),
});
sandbox.format_currency = (value) => String(value);
sandbox.erpnext = {
accounts: { bank_reconciliation: { voucher_types: { ...pre_registered } } },
};
vm.createContext(sandbox);
vm.runInContext(fs.readFileSync(SOURCE, "utf8"), sandbox, {
filename: SOURCE,
});
const registry = sandbox.erpnext.accounts.bank_reconciliation;
const dialog_manager = new registry.DialogManager(
"_Test Company",
"HDFC - _TC",
"2024-05-01",
"2024-05-31"
);
return { calls, sandbox, registry, dialog_manager };
}
function loan_repayment_type(overrides = {}) {
return {
is_applicable: (bank_transaction) => bank_transaction.deposit > 0,
get_fields: () => [
{ fieldname: "against_loan", fieldtype: "Link", options: "Loan" },
],
create: () => Promise.resolve({}),
...overrides,
};
}
const deposit = {
name: "ACC-BTN-0001",
deposit: 500,
withdrawal: 0,
date: "2024-05-05",
description: "NEFT",
};
const withdrawal = {
name: "ACC-BTN-0002",
deposit: 0,
withdrawal: 500,
date: "2024-05-05",
};
describe("voucher type registry", () => {
it("keeps types registered before the bundle loaded, after the built-in ones", () => {
const custom = loan_repayment_type();
const { registry } = load_dialog_manager({
pre_registered: { "Loan Repayment": custom },
});
assert.deepEqual(plain(Object.keys(registry.voucher_types)), [
"Payment Entry",
"Journal Entry",
"Loan Repayment",
]);
assert.equal(registry.voucher_types["Loan Repayment"], custom);
});
it("offers every type until a transaction is loaded, then filters by is_applicable", () => {
const { dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": loan_repayment_type() },
});
assert.deepEqual(plain(dialog_manager.get_document_types()), [
"Payment Entry",
"Journal Entry",
"Loan Repayment",
]);
dialog_manager.bank_transaction = deposit;
assert.deepEqual(plain(dialog_manager.get_document_types()), [
"Payment Entry",
"Journal Entry",
"Loan Repayment",
]);
dialog_manager.bank_transaction = withdrawal;
assert.deepEqual(plain(dialog_manager.get_document_types()), [
"Payment Entry",
"Journal Entry",
]);
});
it("rewrites the Document Type options per transaction and resets a value that is no longer offered", () => {
const { dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": loan_repayment_type() },
bank_transaction: { ...withdrawal },
});
dialog_manager.dialog.set_value("document_type", "Loan Repayment");
dialog_manager.show_dialog("ACC-BTN-0002", () => {});
assert.equal(
dialog_manager.dialog.df_properties.document_type.options,
"Payment Entry\nJournal Entry"
);
assert.equal(
dialog_manager.dialog.get_value("document_type"),
"Payment Entry"
);
assert.equal(dialog_manager.dialog.get_value("posting_date"), "2024-05-05");
assert.ok(dialog_manager.dialog.shown);
});
it("keeps the chosen Document Type when the transaction still allows it", () => {
const { dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": loan_repayment_type() },
bank_transaction: { ...deposit },
});
dialog_manager.dialog.set_value("document_type", "Loan Repayment");
dialog_manager.show_dialog("ACC-BTN-0001", () => {});
assert.equal(
dialog_manager.dialog.get_value("document_type"),
"Loan Repayment"
);
});
it("adds a registered type's fields to the dialog ahead of the transaction details", () => {
let received = null;
const custom = loan_repayment_type({
get_fields: (dm) => {
received = dm;
return [
{ fieldname: "against_loan", fieldtype: "Link", options: "Loan" },
];
},
});
const { dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": custom },
});
const fieldnames = dialog_manager.dialog.fields.map(
(field) => field.fieldname
);
assert.equal(received, dialog_manager);
assert.ok(fieldnames.includes("against_loan"));
assert.ok(
fieldnames.indexOf("against_loan") > fieldnames.indexOf("cost_center")
);
assert.ok(
fieldnames.indexOf("against_loan") < fieldnames.indexOf("details_section")
);
});
});
describe("create_voucher", () => {
it("submits through the registered type and reconciles the dialog", async () => {
const created = [];
const reconciled_transaction = {
name: "ACC-BTN-0001",
unallocated_amount: 0,
};
const custom = loan_repayment_type({
create: (...args) => {
created.push(args);
return Promise.resolve(reconciled_transaction);
},
});
const { calls, dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": custom },
});
const updated = [];
dialog_manager.bank_transaction = deposit;
dialog_manager.update_dt_cards = (transaction) => updated.push(transaction);
const values = {
action: "Create Voucher",
document_type: "Loan Repayment",
against_loan: "LOAN-0001",
};
await dialog_manager.reconciliation_dialog_primary_action(values);
assert.deepEqual(created, [[dialog_manager, values, false]]);
assert.deepEqual(updated, [reconciled_transaction]);
assert.match(calls.alerts[0], /ACC-BTN-0001 added as Loan Repayment/);
assert.ok(dialog_manager.dialog.hidden);
assert.deepEqual(calls.routes, []);
});
it("opens the draft in full page and reconciles it once the form is submitted", async () => {
const draft = {
doctype: "Loan Repayment",
name: "new-loan-repayment-1",
__islocal: 1,
};
const custom = loan_repayment_type({
create: () => Promise.resolve(draft),
});
const { calls, dialog_manager } = load_dialog_manager({
pre_registered: { "Loan Repayment": custom },
});
dialog_manager.bank_transaction = deposit;
dialog_manager.dialog.set_values({
action: "Create Voucher",
document_type: "Loan Repayment",
});
await dialog_manager.edit_in_full_page();
assert.deepEqual(calls.synced, [draft]);
assert.deepEqual(calls.routes, [
["Form", "Loan Repayment", "new-loan-repayment-1"],
]);
assert.equal(dialog_manager.dialog.hidden, false);
assert.deepEqual(calls.alerts, []);
const handlers = calls.form_handlers["Loan Repayment"];
assert.ok(
handlers,
"registered types get the after-submit reconciliation hooks"
);
const frm = {
doctype: "Loan Repayment",
doc: { name: "new-loan-repayment-1" },
};
handlers.before_save(frm);
frm.doc.name = "LM-REP-0001";
handlers.after_save(frm);
handlers.on_submit(frm);
const reconcile = calls.call.at(-1);
assert.match(reconcile.method, /reconcile_vouchers$/);
assert.deepEqual(plain(reconcile.args), {
bank_transaction_name: "ACC-BTN-0001",
vouchers: [
{ payment_doctype: "Loan Repayment", payment_name: "LM-REP-0001" },
],
is_new_voucher: true,
});
handlers.on_submit(frm);
assert.equal(
calls.call.at(-1),
reconcile,
"a voucher is only reconciled once"
);
});
it("passes allow_edit through to the built-in server methods", async () => {
const { calls, sandbox, dialog_manager } = load_dialog_manager();
sandbox.xcall_result = {
doctype: "Payment Entry",
name: "new-payment-entry-1",
};
dialog_manager.bank_transaction = deposit;
await dialog_manager.create_voucher(
{
document_type: "Payment Entry",
party_type: "Customer",
party: "_Test Customer",
},
true
);
await dialog_manager.create_voucher(
{
document_type: "Journal Entry",
journal_entry_type: "Bank Entry",
second_account: "Debtors - _TC",
},
true
);
const [payment_entry, journal_entry] = calls.xcall;
assert.match(payment_entry.method, /create_payment_entry_bts$/);
assert.equal(payment_entry.args.allow_edit, true);
assert.equal(payment_entry.args.bank_transaction_name, "ACC-BTN-0001");
assert.equal(payment_entry.args.company_bank_account, "HDFC - _TC");
assert.match(journal_entry.method, /create_journal_entry_bts$/);
assert.equal(journal_entry.args.allow_edit, true);
assert.equal(journal_entry.args.entry_type, "Bank Entry");
assert.equal(journal_entry.args.second_account, "Debtors - _TC");
assert.ok(
calls.form_handlers["Payment Entry"] &&
calls.form_handlers["Journal Entry"]
);
});
it("refuses a document type that is not registered", () => {
const { dialog_manager } = load_dialog_manager();
dialog_manager.bank_transaction = deposit;
assert.throws(
() => dialog_manager.create_voucher({ document_type: "Sales Invoice" }),
/Cannot create Sales Invoice from a Bank Transaction/
);
});
});

View File

@@ -635,13 +635,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
for f in qty_fields:
qty = d.get(f)
if qty:
qty = flt(qty, d.precision(f))
if qty != cint(qty):
precision = d.precision(f)
if abs(cint(qty) - flt(qty, precision)) > 0.0000001:
frappe.throw(
_(
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
).format(
qty,
flt(qty, precision),
d.idx,
frappe.bold(_("Must be Whole Number")),
frappe.bold(d.get(uom_field)),

View File

@@ -14,8 +14,7 @@
"scripts": {
"postinstall": "cd banking && yarn install",
"dev": "cd banking && yarn dev",
"build": "cd banking && yarn build",
"test:js": "node --test \"erpnext/tests/js/**/*.test.mjs\""
"build": "cd banking && yarn build"
},
"devDependencies": {},
"dependencies": {