Skip to content

OpenConstructionERP v14.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Aug 16:46
· 2271 commits to main since this release

Clicking an element in the BIM viewer to see the defects and inspections attached to it works on PostgreSQL. It raised a server error, on every deployment, since the filter was added. The task list accepts a BIM element id and the query behind it asked for JSON array containment, which is the right question, but the column is declared as portable JSON so that SQLite can carry it too, and on that type the containment call compiles to a string comparison rather than to the containment operator. PostgreSQL then refuses the statement outright because the value being compared arrives typed as JSONB, so the request fails rather than returning the wrong rows, which is the better of the two ways for this to go wrong. The same lookup exists a second time for requirements, written later by copying this one, and it carries the cast that makes the containment real along with a comment explaining why the cast is needed. The copy was correct and the original was not, and the comment above the original described the containment it was supposed to be doing rather than what it did. The original carries the cast now, so both spellings of this lookup compile to the containment operator. The test that catches this exists and is not run by any gate, which is being addressed separately.

A contract still in draft can be deleted from the screen it lives on. The rule permitting it has been in the service layer for as long as the feature has existed, refusing anything that has left draft and saying why, and the client function that calls the endpoint was exported and typed. Nothing called it. There was no delete control on any screen, so from outside the product a working endpoint and a deliberate refusal look the same, and it was reported as a refusal. Nothing in the tree could see it either, because an exported function is not an unused one as far as a linter is concerned, the endpoint is routed and covered, and a screen missing a button renders exactly as correctly as a screen that has one. The control sits beside Sign in the contract drawer and appears only while the contract is in draft, behind a confirmation that names what the deletion takes with it rather than asking whether you are sure, in all 29 languages. It is covered by a test that goes red when the draft condition is removed.

The credentials register can be opened from inside the product. The module was installed, routed on the server, permissioned and complete, and no screen led to it, so the only way in was to know the URL, which from outside is indistinguishable from a module that was never installed. It has a route now, a project scoped one beside it, and an entry under People and Assets next to the resources whose qualifications it tracks. It answers what the crew is licensed to do and what the project demands of them, and it reads in all 29 languages rather than in the four the server itself carries. The menu label was the last thing missing and the easiest to miss: it carried an English fallback, so it rendered as English everywhere rather than as a raw key, and no check reports that, because the scan that finds untranslated strings looks for a call with a default value and this was a property.

The securities that make a contract enforceable are held against the contract. Bonds, bank guarantees, retention and parent company guarantees were kept alongside the system that knows the contract dates rather than in it, which is how an expiry passes unnoticed. Each security carries its own type, status, amount and dates rather than one field per contract, because a contract commonly holds several at once and they expire on different days, and they are read back on the contract itself so the instrument and the obligation it secures are on one screen.

A drawing opened in DWG Take Off matches the file it came from. It came back looking nothing like the source, and reloading showed it briefly right before it changed, which is two defects rather than one. The scene being framed was every sheet at once, because the sheet a reader has not picked yet was settled by an effect that runs after the first frame is already on screen. Model space and paper space share neither origin nor scale, so a box drawn around both frames neither: an 18 m building and a 400 mm title block fitted together collapse the title block to a speck drawn over the plan. The sheet on screen is derived now rather than corrected afterwards, so the first painted frame and the frame after somebody touches the sheet strip are the same frame, and a stored pick naming a sheet the current drawing does not have is treated as no pick, which is what stops switching files painting an empty canvas. The union survives for a drawing whose entities carry no sheet at all, where one implied sheet holding everything is the right answer.

The second defect is that text was clamped into a readable band, and it shipped twice. The clamp was removed during 14.4.0 and put back five hours later with a comment arguing it was intended, and both changes are inside that tag, so the release described the fix and shipped without it. Nothing in the suite could tell, because every drawing it tested was small enough that the band never bound. A label now reaches the screen at its authored height times the zoom, so a heading authored eight times the size of a room tag arrives eight times its size, and a drawing carrying an 800 to 1 range of text sizes reaches the screen with that range rather than flattened to 9 to 1. The two are one bug seen from either end, since the size of a label is a function of the viewport and the viewport is a function of the scene, which is how the same 8 mm title block label came to occupy 123 mm of the sheet. Text size is also a reader preference now, applied to the authored height rather than to a clamped result, because applied to a clamped result it can only move every label together and leaves a flattened sheet flattened at every setting.

The same report also says navigation in that viewer is slow, and that is not addressed here. The viewer stops repainting while nothing is happening, which removes idle cost and says nothing about how it behaves under a hand on the mouse, so it is not offered as a fix for that. The rendering half is verified against the exact build the reporter tested rather than against our own description of it, and the performance half stays open.

A firm can write its own cases instead of only reading ours. The 144 shipped cases describe how we think the work is done, and a team that does it differently had nowhere to record that. The editor starts from a blank case or from any shipped one, which copies its steps as editable text in your own language and records where the copy came from. Each step names the screen it happens on, picked from a catalogue built out of the shipped cases themselves so the list cannot drift from the app. A draft saves in any state, and the rules that decide whether it can be shared with the team run on the server while the editor mirrors the two blocking ones, so the share button greys out while you type rather than failing after you press it. The pins that curate the hub used to live in one browser's local storage, so a curated selection belonged to a laptop rather than to the account; they live on the account now, with a one-time upload of anything already stored locally that treats a half finished upload as half finished and keeps the local copy, so a failed migration cannot lose a list. The hub's own filters fold into a single panel that leaves the catalogue itself higher up the page, and the whole editor reads in all 29 languages. The pipelines route is declared while we are here: the sidebar has linked to it since that module landed and the page, canvas, store and templates were all complete, but the route itself was never added, so the menu entry went nowhere.

Invoices can be filed in countries where an invoice is not valid until the government says so. The platform could already build an EN 16931 document and hand it to a network, which is enough where the network is the whole story and not enough anywhere else. Three regimes that look alike from the outside are kept apart: under clearance the authority's identifier is what makes the document legally valid, under reporting the invoice was already valid and the filing is either on time or late, and on a network there is no authority at all, so a delivered document has been delivered and nothing more. That is why there are nine statuses rather than eight. A routed network document is delivered, never cleared, because calling it cleared would tell a reader that a tax authority answered for it when none exists. The adapter never picks the status; it reports whether the platform accepted, and the regime decides what accepting meant. A document whose adapter died before the platform answered stays submitted rather than being resent or reverted, and an operator resolves it from what the portal shows, because resending risks two cleared invoices for one sale and reverting loses the fact that something was sent. No government adapter ships. The reference one is offline, deterministic, and refuses a profile that is not marked sandbox.

Statutory withholding is deducted the way the governing statute defines it rather than the way one country's rule does. Retention was already handled twice over, on a progress claim and on a subcontract ledger, and both release the money back at practical completion. This never comes back: it goes to a tax authority, and whether a payment is subject to it depends on the payee's verification status on the day rather than on anything in the contract. The taxable base is where a plausible shortcut goes wrong. Gross minus materials minus tax reads like the rule because it is the rule in the United Kingdom, while under section 48 EStG the deduction is taken from the Gegenleistung, which includes the tax and carves nothing out for materials, so a flat rule would under-withhold on every German payment. Each subtraction is a property of the scheme, and an unqualified record is judged the strict way rather than the lenient one. An expired verification moves the payee to the higher band rather than quietly keeping the reduced one, which is the direction that does not leave somebody owing a tax authority the difference. Five schemes and four reverse charge rules ship as data, and none of them installs itself: a fresh deployment has an empty regime table until somebody chooses the rates, because rates go stale and a stale rate that arrived on its own is worse than an empty table somebody has to fill.

A payment deadline is treated the way the statute treats it. In the United Kingdom, Ireland, Australia, New Zealand, Singapore and Malaysia the dates around a payment application are fixed by law rather than agreed between the parties, and missing one has a defined legal consequence rather than a commercial one: silence past the payment notice deadline makes the sum applied for the notified sum, and the payer owes it. A register that stores a due date as a plain field cannot express that, because the interesting value is not the date, it is what happens when nobody answers by it. The module holds the regime, computes the due date, the notice deadlines and the final date for payment from the reference date, records the notices that were actually served, and reports the consequence when a deadline passes unanswered. Eight regimes ship as data, so a jurisdiction can be corrected without a schema change. The clock points at a progress claim or a subcontractor payment application by plain id and never by foreign key, so neither of those modules has to be installed for this one to load, and the category is regional, so a deployment that never works in a security of payment jurisdiction can switch it off.

Those three modules answer over the API and have no screens yet, which is said here rather than left to be discovered. A module that is installed, routed and permissioned with nothing in the product leading to it is the exact shape of the credentials gap described above, and naming it is the difference between work in progress and a feature that quietly is not one. Their screens are the next wave.

The sidebar reads in the order the work happens. Project Files was reachable only from the Drawings and Files group, which sits well down the menu, while everything it belongs with is in Overview; it now appears once, beside Cases. The Drawings group follows Estimating rather than preceding it, Estimate Detail moves to after Procurement so the sequence reads estimate, buy, then the line by line detail of what was bought, and Automation and AI moves to the end, which is where a user reaches for it rather than where they start.

The bill of quantities category tree stops offering a level that filters nothing. Every top level category in the shipped catalogue has exactly one child, the unspecified sentinel, and it holds the entire parent count, because that data carries no department level at all. Opening it changed nothing on screen and cost a click, and there are 129 such nodes standing in front of 14967 rows. The tree lifts the grandchildren one level up when the sentinel is an only child while keeping the sentinel in the emitted path, because that path is positional and dropping a segment would match section names against the department column. The label read "(Uncategorized)", which is not true of an item classified at three of its four depths, and it now reads "(Not specified)" in all 29 languages.

Drilling into a cost category whose next level is empty returns the rows it counted. It showed "no cost items found" while the node itself displayed a count, and on the shipped catalogue that is the normal result of two clicks rather than an edge case. The tree mints the unspecified sentinel in code for a level that is null, missing, empty or whitespace, so no row carries it as a value, and the filter compared that invented literal against the column and matched nothing. The count came from the coalescing side and the filter came from the literal side, and only one of them knew the sentinel was a stand in. Both ask the same question now, through one predicate rather than a copy per caller. The item search and the tree's own parent path walk were written separately and were wrong in the same way, so a search could not scope to an unspecified branch either.

The rate limit in front of the AI rule builder stands in front of the endpoint the product actually calls. It shipped on a route nothing calls, while the panel posts to an older sibling where the AI path builds a real provider caller with no ceiling ahead of it, so a scripted client holding a valid token could drive unbounded cost in a tight loop. A limit written on an endpoint with no caller reads green from every angle: the dependency was there, a test asserted the refusal against it, and none of it sat between anyone and the bill. Both routes carry the guard now, and its test is named in the lane that blocks a merge rather than living where nothing runs it.

Three routes ask for a permission that exists. Enabling a module, disabling a module and fetching the daily reference rates were gated on the bare word admin, which no module registers, and an unregistered key is refused for everyone while an administrator short circuits the check above it, so all three behaved as administrator only and nothing could tell the difference. Nothing was over granted, but the permission matrix raises on an unregistered key, so unlike every other enable and disable shaped permission in the system these three could never be delegated to a manager, and the name sat unclaimed in the global namespace, so the day any module registered a permission spelled admin the access policy of all three would have changed with it. The two module routes use the system permissions that were being registered all along and that nothing was using, and the rate fetch gets the permission registration its module never had along with the startup hook that runs it, because the loader imports a module's router, models, hooks, events and validators automatically and never its permissions.

Opening a link from the desktop menu no longer puts a black command window on screen ahead of the browser. A console subsystem process spawned from a windowed application allocates and shows a console of its own, and that window belongs to the launcher rather than to the browser, which is what made clicking a link look like the app opening a window of its own.

The release check for the desktop build now runs what it builds. It assembled the sidecar, confirmed the files it needed were present, and shipped it without once starting it, which is how a build that cannot start reached a user. A build that does not start at all fails here rather than there.

That check does not settle the macOS report against 14.4.0, and neither does the signing change that was written for it, so this release does not claim that failure is fixed. Both were reasoned from the error text rather than from the file that fails, so the file was read instead: the 14.4.0 sidecar was taken apart member by member, along with two later builds that differ only in how the packer is told to sign, and all three are the same artifact in this respect, 402 embedded binaries, every one of them signed ad-hoc with no team identity at all. Nothing inside the archive can disagree with the process about an identity none of them carries, so whatever supplies one on the affected machine comes from outside the file. Every macOS build now reads that property and fails if a member ever does arrive carrying a vendor identity, which is a tripwire for the day a dependency changes rather than a cure for the report that prompted it. The packer is pinned to an exact version for the same reason: when one build behaves differently from the one before it, the first question is whether the packer changed, and a version range cannot answer that afterwards. The structural answer, which removes the comparison rather than satisfying it, is to stop unpacking a runtime at startup and ship it already in place, and that is a rebuild of the application package rather than a line in this release.

Everything above reads in all 29 languages. Roughly 320 keys per language, covering the credentials register, contract securities, the drawing take off text controls and the rest of the case editor. The gaps were measured from the locale files themselves rather than from the extraction scan, because a key assembled at runtime is invisible to a scan that only sees a literal call, and that is exactly where they were. Where a language already had a word for a case, the whole namespace was brought onto it rather than leaving the new keys and the old ones naming one thing two ways. Swedish keeps two words on purpose, one for a walkthrough and one for a tracked clash, because those are different things.

Approving a timesheet no longer fails because of the daywork sheet it writes afterwards. The write through was deliberately best effort, and the code said so, catching every failure and logging it so an approval is never held hostage by it. On PostgreSQL that promise was not kept, because a failed statement puts the surrounding transaction into an aborted state and everything after it fails too, so the hours that had already been accepted were lost along with it and the error named nothing to do with daywork. It runs inside a savepoint now, which confines a failure to the daywork writes. Daywork sheet numbers are taken from the highest number already in use rather than from a count of rows, since a count only produces a free number while the numbers run from one with no gaps, and a deleted sheet is enough to make it hand back a number somebody already has. Field time also seeds demo data now, having shipped without a seeder, so a demo project showed the module empty, which reads as a module that does nothing.

The dedicated PostgreSQL suite tests against the schema it describes. The translation cache declares its table the moment its module is first imported, and nothing imports it until something asks for a translation, which happens well after the suite has built the schema. The table was therefore in the model metadata and not in the database, so the check that sweeps every text column for leaked seed wording asked PostgreSQL for a relation that did not exist. That module is registered with the others now, so the schema the lane builds and the metadata it reads back describe the same database.

Full Changelog: v14.4.0...v14.5.0