Repository navigation
Releases: Ekwo-ai/ekwo-os
Release list
Ekwo OS v0.16.0
Added
- The
crmmodule: the record of a relationship, kept on the socle's
contacts (module migration20261019175503). A schema of its own —
cards, segments and their members, pipelines and stages, deals and
activities — every row naming its contact by a composite key with its
company. An agent or a screen callsupsert_card,log_activity
(idempotent by source and reference),join_segment,leave_segment,
open_deal,move_deal(a won deal makes its party a customer),
card_ofandsearch_cards. Row level security through
module_enabled()and the module's owncrm.readandcrm.write; the
company archive carries every table ofcrm;merge_contacts()folds two
cards or memberships into one. It posts nothing to the ledger, and a
company enables it on its own (ekwo module enable crm). - The PDF of a validated invoice carries the QR code of its
verification. When the tax authority validated a document under a
clearance regime, or its fiscal record is chained under a chained regime,
@ekwo-ai/invoice-pdfprints the QR code of the verification link and,
beside it, what the country pack says to print. The brick encodes the QR
code itself (ISO/IEC 18004, byte mode) and draws it as vector modules,
checked by decoding every case with an independent decoder. A clearance
regime whose issuer builds the link declares
clearance.verification_urlper environment; a validation recorded
without a link gets it built, or is refused with
clearance_verification_unbuilt.ekwo einvoice clearancetakes
--environment,einvoicing_clearancetakesenvironment. - A line carries what its electronic invoice format asks (socle
migration20261019170000).products.attributesand
document_lines.attributeshold facts the books had no column for — the
entry of the authority's list of goods and services a line is, why its
tax is exempted. A line filled in from a product takes what it does not
carry, a credit note copies the lines it undoes, and a posted line is
frozen with them.create_document,update_document_lines,
add_document_line,create_productandupdate_producttake
attributes;--linetakesattr.<name>=<value>. - A sale in another currency carries its tax in the company's currency.
The source of an electronic invoice gives, for a document in another
currency than its company's, the company's currency, the rate the
document was posted at and the tax of each tax code as its entry posted
it;@ekwo-ai/myinvoiswritesTaxCurrencyCodeand every tax amount in
it. - A free Ekwo Cloud account is offered once, at the moment it helps. One
line afterekwo initand after a firstcompany export, remembered per
person in the configuration directory; one paragraph of the instructions
of the MCP server started from the environment. Silent under--json,
outside a terminal, in CI, withEKWO_NO_CLOUD_OFFER, or once
settings.jsonsays"cloud_offer": false. Nothing is sent to Ekwo. - Malaysia 0.4.0. MyInvois builds its verification link as
{envbaseurl}/{uuid}/share/{longId},myinvois.hasil.gov.myin
production andpreprod.myinvois.hasil.gov.myin the sandbox, from the
SDK FAQ, now a source of the pack. Every tax underEcarries the
exemption_reasonthe provision it cites gives, and a line or a product
names its own classification, checked against LHDNM's list (CV302).
Changed
- The schema is 0.16.0 (
20261020000000), and so is the floor of the
packages.@ekwo-ai/corereadsdocument_lines.attributesand
products.attributesto write an electronic invoice, and@ekwo-ai/mcp
writes them, neither of which a 0.15.0 database has. An installation runs
ekwo migratebefore upgrading the packages. The end-to-end run against a
real Supabase project was not run for this release.
Ekwo OS v0.15.1
Changed
- The schema stays 0.15.0, and so does the floor of the packages. This
release redefinessuggest_duplicate_contacts(),filing_tax_movements()
andprepare_filing()and fillstax_filings.due_date, a column 0.15.0
already has; no package reads anything a 0.15.0 database lacks. An
installation at 0.15.0 runsekwo migratefor the two migrations
(20261019140000,20261019161437) before installing Malaysia 0.3.1. The
end-to-end run against a real Supabase project was not run for this
release.
Fixed
- A pair of contacts by name weighs its words (socle migration
20261019140000, decision 0075 amended).suggest_duplicate_contacts()
proposed "LG SERVICES" and "ASBL UCM SERVICES", "Parking" and "Parking
Cathédrale SA", "DKV Belgium" and "APPLE RETAIL BELGIUM BV": one shared
trade, place or legal form. Each word now weighs 1 over the contacts of the
company that carry it, every word of the name counts at any length, and a
pair needs the rarest significant word of the shorter name in the longer,
with what only one of the two says weighing no more than what they share;
the words the longer adds below the minimum length — a legal form — are not
counted against it. The same name is always a pair. Scores, identifiers
(VAT, registration, bank account) andcontact_candidates()are unchanged. - A return settles the tax of its own form.
filing_tax_movements()—
whatsettle_filing()clears andlock_filed_period()waits for — summed
every tax line of the period, and so carried into the debt of the periodic
return a tax no box of it prints: a self-assessed tax on a service bought
from abroad, a withholding, a provincial tax beside a federal return. It
now keeps a tax line whose box is on the filing's form, or a tax line with
no box whose tax prints a box of that form through another posting and
which is not on the account a tax due on collection waits on
(20261019161437). A period in which nothing else moved refuses with
nothing_to_settle. No figure of any return changes. On the golden years,
the settlement now leaves alone: the self-assessed or withheld taxes of GA,
GH, GN, IT, KM, KR, KZ, MY, PE, PK, SG, TH and VN; the blocked input tax
ZM and ZW book on an expense account; and the cash-basis tax still waiting
on its transition account in AU, AZ, BF, BJ, CI, CL, DZ, FR, GA, IE, MA, MX,
NZ, TG and ZA, settled in the period it is collected, the period its box
reads. - A filing knows the day it is due.
prepare_filing()writes
tax_filings.due_datefromfiling_deadline()— the pack's
deadline.rule— and fills it on a draft that has none; the filings
already written get theirs. The debtsettle_filing()posts matures on it.
A pack with no rule, ordepends_on_taxpayer, leaves it empty. - Malaysia 0.3.1. The README says that settling SST-02 leaves
2102, the
self-assessed Service Tax ofMY-P-SVT-IMPORT, for form SST-02A, which the
pack does not carry, and how to clear it with one entry.schema_min
rises to20261019161437.
Ekwo OS v0.15.0
Added
-
Fiscalisation by clearance: the invoice the authority validates before it
is issued. A third regime ofeinvoicing.fiscalisation, beside the
chained record and the report, modelled on what MyInvois, KSeF and SdI
share: aclearanceobject, exclusive ofhash, says where the
verification link the invoice carries comes from (returned,computed,
none) and, as ISO 8601 durations counted from the validation, how long the
issuer may cancel (cancellation_window) and the buyer reject
(rejection_window), each left out where the law grants none (kind
clearanceonce normalised). For an obligation phased by size, each
mandatory_fromentry may carryturnover_above(an amount and its
currency) and its own text, andexemptionlists the dated thresholds below
which a taxpayer is exempted, oldest first;ekwo pack checkrefuses steps
whose thresholds do not fall, two currencies, thresholds out of order and a
clearance regime that keeps invoices back (none). Blocks that declare
neither compile exactly as before. The module, version 1.2.0
(20261019120000), keepseinvoicing.clearances— one submission per
attempt,submitted,validated(identifier, moment, verification link),
rejected(by the authority or the buyer, with the reason) orcancelled—
withrecord_clearance_submission()andrecord_clearance_answer(), which
raise on every transition that cannot have happened and on a window that
has closed, and append each answer verbatim tofiscal_events. Neither
calls the authority: the transport is the host's.recordClearanceSubmission()
/recordClearanceAnswer()backekwo einvoice clearanceand the MCP tool
einvoicing_clearance;fiscalisationStatus()names, under a clearance
regime in force, the posted sales without a validation and counts those
validated, waiting, rejected by the buyer, or cancelled at the authority and
still posted;fiscaliseDocument()refuses to chain under it
(fiscal_regime_without_chain). The Malaysian pack (0.2.0) declares
MyInvois as a clearance regime in formatmyinvois, with its link returned
by LHDNM and its two 72-hour windows, the four steps of its calendar by
turnover and the RM3,000,000 threshold of exemption of the e-Invoice General
Guideline 4.8, and its e-invoicing profile is nowmyinvois, the document
submitted to the MyInvois API, in place ofpint-my, the Peppol channel. -
A ledger line that names nobody is named once (socle migrations
20261018180416,20261018210000,20261018210500,20261018230000,
decisions 0070 to 0073). A receivable or payable line posted without a
contact is suggested its contact and named once (suggest_line_contacts(),
set_line_contact()), or left unnamed on purpose (leave_line_unnamed());
open_items()can keep to the lines that name a third party. Several open
items settle together (suggest_item_combinations(),reconcile_many()),
a small difference is booked on the way (reconcile_with_difference()), and
the pairs that leave no doubt are matched in one gesture
(suggest_sure_pairs(),reconcile_batch(), undone by
unreconcile_batch()). A reversal and a credit note are matched with what
they undo as they are posted. The command line gainsekwo line contacts suggestandekwo line contact set, and the MCP server their tools. -
Two country packs more, in the Caribbean. Saint Kitts and Nevis and
Saint Lucia, each at version 0.1.0 andcommunity: a chart of accounts, the
taxes of the country with their articles, the return and the financial
statements, each with its golden year. The packs carry their sources and
have not been read by a professional. -
@ekwo-ai/myinvois, the Malaysian MyInvois e-Invoice, a new brick
underpackages/formats/. From the three views of a posted document and
the LHDNM identifiers the books do not hold — the TIN of each party, the
supplier's MSIC code, the moment of issuance — it writes the UBL 2.1
document LHDNM validates, in JSON or XML: invoice, credit note, debit note
and refund note, the supplier's (01–04) or self-billed (11–14),
version 1.1 by default or 1.0 when asked, with the code tables of the MyInvois SDK and every rule
the document breaks, by LHDNM's error code where it publishes one. It signs
nothing and submits nothing. -
A posted sale is issued as the MyInvois document (socle migration
20261019150000, packmy0.3.0).myinvoisis registered in
EINVOICE_FORMATS, soekwo einvoice validateandissue,
einvoicing_validateandeinvoicing_issuewrite the document of a
posted sale through@ekwo-ai/myinvois, version 1.1 by default, in its
JSON syntax, andfiscalisationStatus()names the brick instead of
format_without_brick. What the brick asks beyond the views comes from
data, never from a country or a format named in the schema:- a pack's
einvoicing.fiscalisationgives the brick an attribute of each
tax (tax_attributes, which the chained regime already had) and of every
line (line_attributes, new), merged into the rows of a format that
reads its regime (EinvoiceFormat.readsRegime); companies.identifiersandcontacts.identifiers(jsonb,
{"<scheme>": "<identifier>"}) hold every identifier of a party that has
no column of its own, by the code of its scheme, and
companies.activity_descriptionthe words of its activity beside its
code;document_headerpublishes them asseller_identifiers,
buyer_identifiers,seller_activity_description, withbuyer_phone;- the moment of issuance is the moment the file is written, or
issued_at
ofvalidateEinvoice()andissueEinvoice(); a credit note adjusts the
invoice it credits with the identifier the authority gave it, read from
einvoicing.clearances; a payment means of UNTDID 4461 is written as the
payment mode of the same meaning.
create_contacttakesregistration_number,phoneandidentifiers
(andekwo contact add --stdinthe same fields);update_company_profile
takesidentifiersandactivity_description.
- a pack's
-
Two contacts of one company are merged (socle migration
20261019090000, module assets migration20261019090001, decision 0074).
merge_contacts(keep, absorb)moves every reference of the absorbed contact
onto the kept one in one transaction — every foreign key tocontacts, read
from the catalogue, posted ledger lines and issued documents included, the
branches whose parent it was, the files attached to it — folds its learned
motifs into the kept contact's, fills the kept contact's empty fields
without overwriting any, and retires it: inactive,contacts.merged_into
naming the kept contact, never deleted. It askscontacts.writeand
reconcile.write, and refusesunknown_contact,same_contact,
already_merged,different_identifiers(two VAT or registration
numbers) andmatching_across_partiesby name. Each merge is recorded in
contact_merges, which the guards of a posted line and of an issued
document read, in that transaction alone, as the one exemption that lets
contact_idmove; the trail getscontacts_merged,line_contact_merged
anddocument_contact_merged. A row naming a merged contact is refused
afterwards (contact_merged), by a trigger that
refuse_merged_contacts_in()attaches to every reference of a schema. The
command line gainsekwo contact merge <keep> <absorb>and the MCP server
merge_contacts. -
The contacts that are probably one party are proposed (socle migration
20261019104812, decision 0075).suggest_duplicate_contacts(company)
answers one row per probable pair of active contacts of the company,
strongest first:keep_id(the one more ledger lines and documents name,
then the older),absorb_id,score,evidence(vat_number,
registration_number,account,name, comma-separated) andbecause.
The same VAT or registration number and the same bank account, on the
contact or in a learned motif, score 1; the same significant words of the
name in any order, or every word of one in the other, score as
contact_candidates()scores a name, capped at 0.600 when the words reach
several contacts. Never a pair across companies, an inactive or merged
contact, a contact and its own parent, or two different VAT or registration
numbers. The score of a name and its cap move intoname_agreement()and
name_among_several(), the comparison of an account intoaccount_key()
and of a number intoparty_identifier(), read bycontact_candidates()
andmerge_contacts()alike. About 40 ms at 2 000 contacts. The command
line gainsekwo contact duplicatesand the MCP server
suggest_duplicate_contacts. -
An analytic value is entered on the line, and read from the ledger
(socle migration20261019130000, decision 0076).document_line_analytics
is the split of a document line, in percent per axis;post_document()
groups the lines by account, tax and split and copies the split of each
group onto the ledger line it writes — the share of a tax nobody gets back
booked on the expense included — so the ledger stays the one source of
every report.cancel_document()gives the credit note the split of the
invoice. A split is refused outside the income and expense groups of
accounts.internal_groupand the fixed assets (account type
asset_fixed) —analytic_not_on_this_account— beyond 100 % on one axis
(analytic_split_over_100), and on a new share of a retired value
(analytic_value_retired); the split of a posted ledger line moves only in
an open period (period_locked,fiscal_year_closed). Itsamountis
derived by the database and a whole split adds up to its line to the cent.
products.analytic_value_idandcontacts.analytic_value_idpre...
Ekwo OS v0.14.0
Added
-
A closed year can be reopened; a final close cannot (socle migration
20261018130000, decision 0031 rewritten).reopen_fiscal_year()no longer
refuses a year whose successor holds posted entries: the close writes
nothing in the year after, which reads the result from the ledger, so a year
reopened, corrected and closed again carries its new result forward with no
entry in the later years, and moves nothing there when nothing changed. A
later year that is closed is reopened first (later_fiscal_year_closed).
close_fiscal_year_finally()setsfiscal_years.finally_closed_aton a
closed year, after every closed year before it; from then on
reopen_fiscal_year()refuses it (fiscal_year_finally_closed) and the
guardfiscal_years_guard_finalrefuses any change to the year, for
everybody,service_roleand the installer included. The act is audited as
fiscal_year_finally_closed. Whether a country requires it is pack data:
defaults.final_close(due,legal_reference,source) compiles into
country_defaults.final_close_due, read byfiscal_year_final_close(). The
French pack (1.21.0) declares it under the PCG, art. 921-4. The MCP server
gainsclose_fiscal_year_finally, andclose_fiscal_yearanswers with
final_close. -
A piece goes on its document (socle migration
20261018102431,
decision 0069). An invoice, a credit note or a receipt is attached to its
document. The triggerattachments_guard_documentrefuses a file on an
entry that a document posted, withattachment_belongs_on_documentnaming
the document to attach to instead; an entry no document posted keeps its own
pieces.docs/import.mdsays how a taken-over history brings its invoices
in as documents, with their PDFs. -
Fiscalisation, a capability of the
einvoicingmodule. A country pack
may declareeinvoicing.fiscalisation— the chained record its law requires
the invoicing software to keep for every invoice: the format and its brick,
the dates, the fields the hash covers, the QR code, the sentence, how
records are sent, the events, each with its article.ekwo pack check
holds it to the schema and to what the schema cannot say; it compiles into
country_defaults.einvoice_fiscalisation(socle migration
20261018014537) andpack describe/describe_packshow it. The module,
version 1.1.0 (20261018015212), keepseinvoicing.fiscal_chainand
einvoicing.fiscal_events, immutable, withappend_fiscal_record(),
verify_fiscal_chain()andlog_fiscal_event(); a record's file is an
issue of artefactfiscal_record. The core registers formats in
FISCALISATION_FORMATS, with@ekwo-ai/verifactufirst, and
fiscaliseDocument()/fiscalisationStatus()backekwo einvoice fiscalise/fiscalisationand the MCP toolseinvoicing_fiscalise/
einvoicing_fiscalisation_status. Nothing is fiscalised on posting: the
status names the sales without a record. The Spanish pack (0.6.0) declares
VERI*FACTU from Orden HAC/1177/2024. -
Fiscalisation without a chain: a reporting regime, and a day only
announced. Ineinvoicing.fiscalisation,hashbecomes optional: left
out, the regime reports the documents' data to the authority and nothing is
chained (kindreportingonce normalised).announced_from, with
announced_byandannounced_on, carries a day announced while no text is
in force, exclusive ofmandatory_from;submission.channeland
submission.cadence(per category of taxpayer and kind of data: the period
and the due rule) carry a batch sent by period;reportednames what is
reported and by whom. Socle migration20261018023930widens the shape
check ofcountry_defaults.einvoice_fiscalisation.fiscaliseDocument()
refuses a reporting regime by name (format_without_brick, then
fiscal_regime_without_chain), andfiscalisationStatus()counts no sale as
missing a record under a reporting regime or one only announced. The
Belgian pack (1.21.0) declares the e-reporting announced for 1 January 2028;
the French pack (1.20.0) the e-reporting of transaction and payment data of
CGI art. 290 and 290 A, with its dates by size and its cadence by VAT
regime. Neither has a brick yet. -
What each product brought in, and cost.
product_report(company, from, to)(migration20261016145816) reads the posted invoices and credit notes
dated in a period and gives, for each product named on a line and on each
side — sold, bought — the documents that named it, the quantity and the
untaxed amount in the company's currency, a credit note counting against
its invoice. Drafts, quotes, orders and lines naming no product say
nothing; a retired product keeps reporting what it did and saysactive
false. Security invoker, so the caller reads whatdocuments.readlets
them read. It is the reading behind a reporting by line of service, such
as the Products screen of Ekwo Cloud: the ledger never knew which product a
line was, anddocument_lines.product_idis the one place that does. -
Every company gets an analysis, whatever its country. A company whose
country pack carries noanalysis.jsonused to get no row from
financial_analysis(), so the Analysis screen of most countries was empty.
The framework pack now carries one,packs/generic/analysis.json(generic
1.2.0): thirteen of the sixteen aggregates written as sums of the eighteen
account types, aspacks/generic/statements.jsonbuilds a balance sheet for
any chart — revenue, other operating income, purchases, depreciation, the net
result, equity, total and current assets, cash, trade receivables, current
liabilities, trade payables and long-term debt. Staff costs, the financial
result and income tax are left out, because no account type tells them apart:
such a company gets no row for them rather than a zero. Migration
20261016065440letsanalysis_templatesandanalysis_ruleshold a
framework's rows, with no country and no chart, andfinancial_analysis()
reads the country pack's rows when the company's chart has any and the
framework's otherwise — the framework whose statements the chart declares,
else the one the installation carries. The function returns a fourth column,
source,packorframework, typedFinancialAnalysisRow.sourcein
@ekwo-ai/coreand explained by thefinancial_analysistool of
@ekwo-ai/mcp. Country seeds and their keys are unchanged. See
decision 0068. -
Six country packs more, in the Caribbean and South America. Antigua
and Barbuda, the Bahamas, Dominica, Grenada, Jamaica and Guyana, each at
version 0.1.0 andcommunity: a chart of accounts, the taxes of the country
with their articles, the return and the financial statements, each with its
golden year. The packs carry their sources and have not been read by a
professional.
Changed
-
The schema of this release is 0.14.0, and the packages refuse an older
one (20261018135109). It carries the time given to an archive, the
analysis of a framework,product_report(), the fiscalisation of a country,
the guard that puts a piece on its document, and the final close of a year.
@ekwo-ai/mcpcallsclose_fiscal_year_finally()and
fiscal_year_final_close()and readscountry_defaults.einvoice_fiscalisation,
and the packsekwo-oscarries writeeinvoice_fiscalisation,
final_close_dueand theframeworkof their statements when they are
installed or upgraded, none of which a 0.13.0 database has, so the floor of
ekwo-os,@ekwo-ai/coreand@ekwo-ai/mcprises to 0.14.0 and an
installation left behind is told to runekwo migrate.@ekwo-ai/verifactu
is published for the first time, as a dependency of@ekwo-ai/core. -
The Estonian pack carries the analysis section. Pack
ee1.13.0 adds
analysis.json: the sixteen aggregatesfinancial_analysis()reads — revenue,
purchases, staff costs, the financial result, the equity, the current assets,
the cash, what customers owe and what is owed to suppliers — mirrored on the
lines of the balance sheet and of income statement scheme 1 of the
Raamatupidamise seadus, side included for the tax and suspense accounts the
balance sheet shows on either side. Until now an Estonian company had no
analysis at all, and the Analysis screen of a client drew empty charts: a pack
that has not said what its accounts mean gives none, by design. The golden of
the pack holds the figures beside its statements. Nothing in the schema changes. -
The Estonian pack is
provenon the real books of a company. Packee
1.12.0 reproduced, to the cent, every non-empty line of the balance sheet and
of the income statement of the 2025 annual report an Estonian OÜ filed with
the e-Business Register, on books rebuilt from its accounting provider's
exports — an opening balance at 31 December 2024, then 70 documents and 38
monthly camt.053 statements settled to the last line.certification.evidence
says what was reproduced and that the monthly VAT return (KMD) was computed
and recorded but not compared with the figures as filed. Months before
July 2025 are refused by name, because the pack carries the form in its
wording in force from 1 July 2025 only. Nothing in the schema changes. -
The README says where to get help, and what the books answer at a
glance. A Support section names the three ways — issues and discussions
in the open, e-mail with Hosted and Pro, and Ekwo Consulting on quote for
tailor-made work and dedicated support — and What ships today lists the
compass:financial_analysis()and the seven figures the Analysis screen
and the MCP tool read from it.DISCLAIMER.md2.2 names Ekwo Consulting as
a trade name of EKWO OÜ beside Ekwo. Nothing in the schema or the p...
Ekwo OS 0.12.2
Added
-
The result of the year in progress has a sub-line of its own where the
balance sheet's line mixes it with something else. Since a balance sheet
drawn before the close carries the result nobody has closed, a Belgian
rubric 14 "Bénéfice (Perte) reporté(e)" showed the profit brought forward
and the year in progress as one figure. Every balance sheet of every pack
now says, in the newresult_in_progressofstatements.json, whether the
line that receives the result is the resultalone(FranceDI, 51
schemes) orcombinedwith other balances (Belgium14, every scheme that
closes into retained earnings, the SYSCOHADACJ; 77 schemes), and a
combined one names the label of its sub-line in its own languages.
financial_statement_of_kind()returns the result in progress as a row
<line>:result_in_progressunder that line;financial_statement(), the
totals and every filing are unchanged.statement_account_matches()gains
the columnin_progress, andstatement_figures()is the one place a
statement is summed.ekwo pack checkrefuses a balance sheet that leaves
the rule out or contradicts its own scheme and chart. Migration
20261011120000, which every pack now names as itsschema_min. -
The corporate income tax module speaks to an agent. Eight MCP tools
under thetaxprefix, offered once the module is installed:
tax_estimate(the year line by line, each line with its article),
tax_parametersandtax_declare_parameter(the facts the country asks a
company to declare, written only as the user states them),tax_adjustments
andtax_declare_adjustment(the rules in force and what the company names
under each: an account or an amount),tax_losses,
tax_record_computationandtax_computations. The prompt
tax_review_yearwalks through where the tax of a year stands and what is
still to declare. Calling a computation final stays the owner's act, outside
the server. A module may now ship prompts beside its tools, under the same
prefix. No migration. -
The corporate income tax module books its provision and plans the
prepayments (moduletax1.1.0).tax.book_provision(computation)books
what a recorded computation says as the charge of its year, on the expense
and payable accounts of the pack, throughpost_module_entry(): the
difference with what the expense account already carries for the year, so a
charge booked by hand is not booked twice and a later computation books only
what moved; one entry per computation.tax.prepayment_plan(company, fiscal_year, at, tax)reads the schedule of the pack: each instalment on its
day, its share of the final tax of the year before — nothing under the
exemption — or, against a surcharge, the same amount at every instalment
still to come that leaves none, never more than the tax still unpaid. What
the company paid in advance is declared in a new table,tax.prepayments,
and counted towards the first instalment due on or after it. The golden
tests book the provision of every worked example of every pack and hold the
tax to the cent.
Changed
-
The schema of this release is 0.12.2, and the packages refuse an older
one (20261011163457). It carries the threeresult_in_progresscolumns
ofstatement_templatesand the balance sheet that prints them, and the
four integrity fixes below. Every packekwo-oscarries writes those
columns when it is installed or upgraded, which a 0.12.1 database does not
have, so the floor ofekwo-os,@ekwo-ai/coreand@ekwo-ai/mcprises to
0.12.2 and an installation left behind is told to runekwo migrate. -
The licences, the CLA, the disclaimer and the privacy notice name EKWO
OÜ, the name the Estonian commercial register entered on 8 October 2026
(registry code 14510673).
Fixed
-
An import proposes the detail account behind a padded code. A FEC with
ten-digit codes (110000000,4010000000) got no proposal for most of its
unmatched accounts: once the padding zeros are set aside, a heading and its
detail account read the same, and the tie was no answer. On a tie, only the
accounts that take entries are kept now. -
An import proposes a journal from what its entries touch, never its
name. An opening journal went to the general journal, and the sales and
bank journals with it. The opening is now the journal whose entries all fall
on the first day of a year and touch no income or expense account, or the one
going by an opening code the packs give, its entries on one day; sales,
purchases and bank are the journals whose every entry touches a receivable
and an income account, a payable and an expense account, or a bank account.
Each proposal carries its reason underjournal_reasons, whichekwo import
shows. -
The French chart carries the capital called and paid, the tax withheld at
source and the research tax credit (packfr1.17.2):101300(PCG
1013),442and442100(PCG 4421),699and699100for the research
tax credit, which BOI-BIC-RICI-10-10-50 § 510 books on 699. The credit is not
taxable income: a new deduction rule takes the balance of699100out of the
result the corporate tax estimate starts from. The golden year is unchanged.
Security
-
A bank line is settled by the function that settles it
(20261011154807; issue #91, reported by Pieterjan Muller).
bank_transactions.entry_idandstatewere plain columns under
bank.write: settingentry_idback to null let the same money be settled
a second time, writingreconciledtook a line nothing had settled out of
every list, and the amount of a line already booked as a payment could
still move.bank_transactions_guard()now lets a person move a line
betweenpendingandignored, and leavesreconciledandentry_idto
settle_from_statement(), which raisesekwo.settling_statementaround its
one update, honoured for a caller holdingreconcile.write
(bank_transaction_settled_by_rpcotherwise, on an insert as on an update).
A settled line keeps its amount, day, currency and bank account
(bank_transaction_settled).settle_from_statement()also locks the line
it reads, so two calls for one line wait for each other. The demo company
raises the setting for the one settled line it writes. -
A matching settles the items of one counterparty
(20261011155443; issue #89, reported by Pieterjan Muller).reconcile()
never readcontact_id, so on a receivable account shared by every
customer the payment of one could settle the invoice of another; and
reconciliationswas writable by hand underreconcile.write, past every
checkreconcile()makes.reconcile()now refuses two lines that name
different counterparties (reconcile_contact_mismatch); a branch and its
parent are one party, and a line naming nobody — a receipt keyed by hand,
imported history — is attributed by the matching.reconciliations_guard_reconcile()
keeps the table toreconcile()andunreconcile(), which raise
ekwo.reconciling, honoured for a caller holdingreconcile.write
(reconciliation_by_reconcileotherwise). Grants and policies are
unchanged, and the cascades of a company and of a line still pass. -
A ledger line names a declaration box its tax posting writes
(20261011155947; issue #88, reported by Pieterjan Muller). A draft line
keyed by hand could carry anydeclaration_boxandbox_amount, with no
tax, andvat_return()summed it into that box.entry_lines_box_from_posting()
now asks of every line that names a box that it names a tax, and that a
posting of that tax writes that box on that side — ataxposting for a
tax line,baseortax_on_baseotherwise
(declaration_box_without_posting).post_document()and
settle_cash_basis_tax()already write exactly that; a reversal keeps the
box of the line it mirrors, even when the posting has since moved. Lines
already in a ledger are not rewritten. -
A ledger line keeps the boxes it was posted in (
20261011160431;
issue #90, reported by Pieterjan Muller).vat_return()read the boxes a
line prints in and the form it is on from today'stax_postings, which a
member withsettings.writeedits andpack_upgrade()rewrites, so a
return already filed for a locked period could come out with other
figures. Two columns,entry_lines.declaration_boxesand
entry_lines.report_code, are now frozen when the line is written, from
the posting that writes it, byentry_lines_box_from_posting(); a reversal
takes those of the line it mirrors, and a change by hand is refused
(declaration_boxes_from_posting). The rule is written once,
declaration_boxes_of(), and is the onevat_return()applied at reading:
the backfill writes on every line what the return read for it, and every
pack's golden year returns the same declarations to the cent after the
upgrade.vat_return()reads the line, and falls back on the postings only
for a line restored from an archive written before this release.
Ekwo OS 0.12.1
Added
-
Every country pack says what a service bought from abroad costs in
tax. A software subscription, hosting or an API billed by a supplier
abroad is the most common purchase of a young company, and 44 packs that
tax purchases had no code for it, so such a purchase was booked with no tax
at all. Each of them now either carries the tax the buyer owes — a reverse
charge, a self-assessment, the withholding of the supplier's tax, each with
its boxes, its credit-note mirror, the period its credit falls in and one
such purchase in the golden year — or says, in the newnot_taxedof
pack.json, with its article and source, that its country leaves the case
untaxed in the buyer's hands (Bolivia, the Dominican Republic, Guatemala,
Honduras, Sri Lanka, the United States for California, Uruguay). Taxes were
added for Algeria, Armenia, Azerbaijan, Benin, Bosnia and Herzegovina,
Cameroon, the Central African Republic, Chad, Colombia, Costa Rica, the
Czech Republic, the Democratic Republic of the Congo, Ecuador, El Salvador,
Ethiopia, Georgia, Ghana, Greece, Guinea, Kazakhstan, Korea, Latvia, Mali,
Mexico, Nicaragua, Niger, Nigeria, North Macedonia, Pakistan (Punjab,
Sindh), Panama, Paraguay, Peru, the Republic of the Congo, Taiwan, Ukraine
and Viet Nam; the Czech, Greek and Latvian packs carry one code for a
supplier of another Member State and one for a supplier outside the European Union;
Malaysia's imported-service tax takes the treatment
foreign_services_received. Where the credit comes a period later
(Kazakhstan, Nicaragua, Peru, Viet Nam, as Argentina) the tax waits on an
account of its own and the README says how it moves; where the buyer
deducts nothing (Benin, the DRC, Ghana, Korea, Taiwan) it is a cost.ekwo pack checknow refuses a pack whose taxes include a rated purchase VAT,
GST or sales tax and which does neither, anddocs/packs.mdsays how to
write either. -
An invoice wears the colour of its company. Two nullable columns on
companies,invoice_accent_color(#rrggbb) andinvoice_logo_position
(leftorright), checked by the database, written by
update_company_profilewithcompany.writeand read back by
get_company.ekwo doc pdfandrender_invoice_pdfread them and hand
them to@ekwo-ai/invoice-pdfas its theme, with no new argument; null is
the brick's default. The packages now read columns a 0.12.0 database does
not have, so this release raises the schema floor.
Changed
- The schema of this release is 0.12.1, and the packages refuse an older
one (20261011082857). It addsyear_result_account()and
statement_rule_catches(), changes what a balance sheet reads, and carries
the two invoice columns ofcompanies.get_company,ekwo doc pdfand
render_invoice_pdfread those columns, which a 0.12.0 database does not
have, so the floor ofekwo-os,@ekwo-ai/coreand@ekwo-ai/mcprises to
0.12.1 and an installation left behind is told to runekwo migrate.
Fixed
-
A balance sheet drawn before the close balances: the result of the year
in progress stands on equity (20261011081256). Until a year was closed,
its result stayed on the income and expense accounts, which a national
balance sheet does not read: a Belgian company in the middle of its year
printed TOTAL DE L'ACTIF 3 025,00 and TOTAL DU PASSIF 525,00, the 2 500,00
of the year missing, while the trial balance said the books balanced.
statement_account_matches()now moves the balance of every income and
expense account that no line of a balance sheet catches onto the account
the close would leave the result on — the current-year result account of a
pack that closes into one (DIin France), retained earnings otherwise
(rubric14in Belgium) — and the pack's own rules put it on its line.
Assets equal liabilities and equity to the cent, for a profit and for a
loss, in all 127 packs that carry a balance sheet. After a close nothing is
moved, because the closing entry has already emptied those accounts, so
nothing is counted twice; a period that stops inside a closed year shows
the result to that day. The choice of account isyear_result_account(),
whichfec_lines()now reads too, so the à-nouveaux of a year nobody
closed and the balance sheet put the result in one place. A scheme that
already catches income and expense on a line of its own — the generic
framework, and 17 packs whose balance sheet does the same — is unchanged.
The golden balance sheets of the 110 other packs are rewritten: equity and
the total of liabilities rise by the result of the golden year. -
Eight packs book or declare their self-assessed and import VAT as the
law of their country does. Bulgaria, Croatia and Poland credit the tax a
buyer owes on an intra-EU acquisition or a service from abroad to output
VAT (6831, 6401, 7710), so the supplier is owed the price alone and the
deduction stays on input VAT; their boxes are unchanged. Portugal deducts
import VAT collected by customs on campo 24 and owes it on 2781, keeping
campos 18/19 for the art. 27, n.º 8 option (PT-P-IMP-23-DP). Morocco makes
VAT due on collection the default, as art. 95 CGI does, with the debits
option on codes of its own. Norway reports imported goods and services from
abroad on mva-melding codes 81 and 86, with their deductions on 81F and 86F.
Paraguay declares credit notes on the side of Formulario 120 (version 4)
they belong to. Korea reports every tax invoice received in box (10) and
takes the non-creditable tax back out at (16). Each golden year follows.
Security
- The file of a declaration is read as a declaration
(20261010203452; decision
0039). The deposits of a
declaration askedfilings.read, and the two files they name — what was
sent and the receipt — askeddocuments.read, like every attachment: a
member whosefilings.readwas withdrawn and who keptdocuments.read
read them, within their own company. An attachment of a declaration, or
one a deposit names, is now read withfilings.readand written with
filings.write, by a person and by a machine key alike; every other
attachment keepsdocuments.readanddocuments.write. The policy finds
the deposit throughattachment_is_filing_deposit_file(), a definer
function, because the deposits are hidden from precisely the member this
is about.
Ekwo OS 0.12.0
Added
-
The privacy page says the agents of Ekwo Cloud may learn from the books.
Version 1.3 ofPRIVACY.mdadds the improvement of the agents, including
training on examples drawn from the books, to the purposes for which Ekwo
reads them, under the same legitimate interest and the same right to object;
the statements that the site carries no measurement tool and that no model is
trained on the books are replaced by what holds today and what consent a
measurement tool would ask for. -
The privacy page covers the projects Ekwo hosts. Version 1.2 of
PRIVACY.mdopens with the choice of where the books live — a Supabase
project of one's own, or one Ekwo hosts — and how a project moves from one to
the other; it lists the books of a hosted project and of the trial instance
among what Ekwo Cloud holds, names the optionalekwo registerand what it
sends, and gives the retention of a hosted project. -
An invoice you can send: its PDF, with your logo. A new brick,
@ekwo-ai/invoice-pdf, renders the visual PDF of a sale invoice or credit
note from the four views the books already publish —document_header,
document_line_items,document_tax_summary,document_legal_mentions—
in one sober layout, A4 or Letter, multi-page, with no country in it: the
words of the layout are English or the caller's, the sentences the books',
the amounts and datesIntl's in the document's language. Noto Sans and
Noto Sans Thai are embedded (OFL); other scripts through a font the caller
adds; right to left is refused by name.ekwo doc pdf <document>(also
ekwo document pdf) and the MCP read toolrender_invoice_pdfread the
views as the person signed in, fetch the logo the company names by URL —
from a public address only — and render it;--factur-x(factur_x)
embeds the CII the e-invoicing adapter writes from the same books, which
makes it a Factur-X PDF/A-3. Nothing is recorded: keeping the copy that was
sent, and sending it by e-mail, come next. -
The invoice PDF is a PDF/A-3b by veraPDF's measure, plain or as
Factur-X: no failed rule of the PDF/A-3b profile in the examples of
@ekwo-ai/invoice-pdf.embedFacturX()of@ekwo-ai/factur-xnow writes
an XMP packet that says what the PDF's information dictionary says. The
layout gains a small theme — accent colour, typeface, side of the logo — and
the figures it prints are tested equal to the CII embedded beside them. -
The e-invoicing bricks read what they receive.
readUbl()of
@ekwo-ai/peppol-ublreads a received Peppol BIS Billing 3.0 invoice or
credit note (UBL 2.1);readCii()andreadFacturX()of
@ekwo-ai/factur-xread a Factur-X or ZUGFeRD invoice of any profile, from
its XML or out of its PDF. Both return the sameReceivedInvoice— parties
with their schemes, dates, lines, VAT breakdown, totals as exact decimal
text, payment means, attached documents as bytes — so that a received
invoice becomes a purchase draft the same way whichever syntax carried it.
The EN 16931 arithmetic is reported inviolations, never corrected; a
file that is not an invoice throws anInvoiceFileErrorwith a code. The
Peppol brick also checks a participant identifier
(validateParticipantId(), with the check digits of seven schemes) and
computes its DNS name on the SML (smlHostname()), without a network call. -
The
einvoicingmodule: the electronic invoice of a posted sale, and
every time it was sent. A fourth module, in its own schema. From a posted
sale invoice or credit note it writes the file in the profile the company's
country pack declares —peppol-bis-3by@ekwo-ai/peppol-ubl,
factur-x-en16931by@ekwo-ai/factur-x, held to the posted totals to the
cent — and keeps the exact file with its SHA-256 and every rule of the
format it breaks. A file that breaks one is not sent, and the database says
which, word for word. Each sending is a row with a state that only moves
forward —prepared,submitted,accepted_by_access_point,delivered,
rejected,failed: a reference from a transport is not a delivery — and
every answer of the service is kept verbatim. A rejected document is issued
and sent again, as a second row. A profile no brick writes is refused as
format_without_brick. Capabilitieseinvoicing.readand
einvoicing.send; sending is not posting. The module knows no country,
names no provider and posts nothing. See
modules/einvoicing. -
A transport contract for electronic invoices, in
@ekwo-ai/core:
send,status,receiveandlookup, with no credential crossing it.
The one transport that ships writes the file to a folder and reads the
receipts left beside it; network transports come later, behind the same
contract. -
MCP tools
einvoicing_validate,einvoicing_issue,einvoicing_status
andeinvoicing_list, andekwo einvoice validate | issue | status | list.EKWO_EINVOICE_DIRECTORY(or--to) names the folder files are
sent to; unset, they are issued and kept, and nothing leaves. -
Once a sending has left,
documents.peppol_statusandpeppol_message_id
carry its state and reference, which is what keepsunpost_document()from
taking the document back to draft. -
A filing is proved by its hash (
20261007192418; decision
0066;
docs/filing-proofs.md). The sha256 of the exact
bytes of a declaration, of the annual accounts of a financial year, or of a
document, is committed to Bitcoin through the public OpenTimestamps
calendars. Only the hash leaves. Anybody holding the file can later check,
without an account, that it existed no later than a given block. The same
for every country and every pack.filing_proofskeeps one row per hash and per method, with the proof bytes,
the status (pending,complete) and the anchor (chain, block height, block
time, block hash).record_filing_proof()andupgrade_filing_proof()are
the only way in and need the new capabilityfilings.prove, onownerand
accountant. A complete proof is frozen.tax_filing_values_sha256()hashes
the frozen figures of a declaration withcanonical_json()and is recorded
with each of its proofs.filing_proof(sha256)is a public door like
shared_document(): the anonymous role may call it, and it returns the proof
of one hash and nothing about who proved it or what it is about. Proofs
travel with a company's archive.@ekwo-ai/corereads and writes the.otsformat, submits to the calendars,
upgrades a pending proof, and dates it from an Esplora block explorer after
checking the block's merkle root:proveFile(),upgradeFilingProofs(),
lookupFilingProof(),checkProof(). No new dependency.ekwo proof stamp <file> --filing | --year | --document,ekwo proof upgrade(made for a schedule) andekwo proof verify <file> [--ots] [--out].The method column also accepts
eas, the Ethereum Attestation Service.
Nothing writes it yet:docs/filing-proofs.mdevaluates it as a second,
signed layer, with its costs. -
23 more country packs, bringing the set to 130: am (Armenia), ao
(Angola), az (Azerbaijan), bd (Bangladesh), bm (Bermuda), bn (Brunei
Darussalam), et (Ethiopia), gg (Guernsey), hn (Honduras), je (Jersey), kw
(Kuwait), ky (Cayman Islands), lk (Sri Lanka), mo (Macao), mu (Mauritius),
mz (Mozambique), ni (Nicaragua), pk (Pakistan), qa (Qatar), sv (El
Salvador), vg (British Virgin Islands), zm (Zambia) and zw (Zimbabwe). Each
carries its own chart of accounts, its taxes and the return its country
files, as data rather than as code, and each arrives at the status
community— nobody has reviewed it, and the pack says so itself. -
packs/ar0.2.0: the tax on services received from abroad. A
responsable inscripto who buys a service performed abroad and used in
Argentina owes the tax on it (Ley de IVA, art. 1, inciso d), and art. 4,
inciso g)), pays it within ten business days of each taxable event outside
the monthly return (Resolución General (AFIP) 549/1999), and computes it as
crédito fiscal only in the following period (art. 12, last paragraph). The
newAR-P-EXT(foreign_services_received, 21 %) puts the base in an
informational boxPCEXT, debits the tax to a new1153, a credit not yet
computable, and credits it to a new2135, the tax to pay, with no box of
crédito fiscal in the month of the invoice. Moving the credit to1151in
the following period is a manual entry, because the format cannot date a
box one period after its document; the README says so, explains
inciso e) — digital services bought by somebody who is not a responsable
inscripto, where a payment intermediary perceives the tax under Resolución
General (AFIP) 4240/2018, which the pack does not post — and puts a
supplier abroad on2112. The golden year gains April and one such
purchase.
Changed
-
The schema of this release is 0.12.0, and the packages refuse an older
database by name (20261010181201).@ekwo-ai/coreandekwo proof
callrecord_filing_proof()andupgrade_filing_proof(), which a 0.11.1
database does not have — so the floor ofekwo-os,@ekwo-ai/coreand
@ekwo-ai/mcprises to 0.12.0, and an installation left behind is told to
runekwo migraterather than failing in a call nobody can place. -
The privacy page says when Ekwo reads your books. Version 1.1 of
PRIVACY.mdreplaces the statement that nobody opens an Ekwo Cloud backup
with what is actually done: Ekwo may read the books of a linked instance and
its backups in the customer's interest (support, checking entries against
the rules of the country, flagging an error) and to improve the software,
its importers and its country packs...
Ekwo OS 0.10.0
Added
-
Corporate income tax, estimated from the books — a third module, code
tax, in a schema of its own (modules/corporate-tax, migration
20260930104417).tax.estimate(company, fiscal_year, at)reads the
accounting result from the income statement the country pack names and
returns, line by line and with the article behind each, the adjustments, the
losses of earlier years as far as they reach, the taxable base, each rate
with the slice it takes, the credits and the figure — called
estimated_tax. It reads and writes nothing, and no country rule lives in
it. A company declares what only it can state — small company or not, the
remuneration of a director — intax.company_parameters; a condition that
is not declared is not met, and the estimate says which. It names the
expenses that fall under a rule intax.adjustments, by account or by
amount, and the losses it carried in intax.losses.
tax.record_computation()keeps an estimate, numbered per year;
tax.finalise_computation()calls one final —tax_due— for a whole year
whose ledger still gives the same lines, and needs the new capability
tax.finalize, which the owner preset holds and the accountant preset does
not;tax.withdraw_computation()takes a final one back. The loss stock
moves with final computations only. The module posts nothing: the provision
entry, the prepayment plan, the declaration forms and the MCP tools are
later versions. Addtaxto the exposed schemas of the project to reach it
over the API. -
A country pack may say how a profit is taxed:
packs/<cc>/corporate_tax.json,
described inpacks/schema/pack.1.jsonand indocs/packs.md, compiled
intosupabase/seed/modules/corporate_tax/. The line the computation starts
from, the accounts the tax is booked on, the parameters a company declares,
the adjustment rules — a percentage, or a formula in a closed vocabulary —
the rates with their thresholds and their conditions, the limit on losses,
the prepayments and the credits. Every figure carries its article and the
day it applies from, read on the first or the last day of the financial year
as the law words it, and a figure that changes is a new dated entry. A
section comes withgolden/corporate_tax.json: fictitious companies whose
tax was worked out by hand, replayed to the cent. The section is open to
every pack: one optional file beside the others, written from the texts of
the country and from nothing else. A pack that carries it gives the ordinary
rate, the reduced rates with their conditions, the disallowed expenses that
could be cited, the limit on losses and the prepayment schedule, and its
README names what the section leaves out and why. A company of a country
whose pack does not carry it yet is told so by name,
no_corporate_tax_rules. The packs that carry it: Australia, Belgium,
France, Germany, Mexico, the Netherlands, Nigeria, Senegal, Singapore, South
Africa, Spain, the United Arab Emirates, the United Kingdom and the United
States.
ekwo pack checkrefuses a section that names a statement, a line, an
account or a parameter the pack does not carry, two versions of a rule in
force on one day, and a section without its worked examples. -
The fixed assets section is carried by fourteen packs.
packs/<cc>/fixed_assets.jsongives the module what a country says about
its fixed assets: the categories with the method and the life each is
usually written off over, how a first and a last year are prorated, whether
a declining balance is allowed and when it turns linear, and how a disposal
is booked — every figure with the text it comes from. The packs that carry
it: Australia, Belgium, France, Germany, Mexico, the Netherlands, Nigeria,
Senegal, Singapore, South Africa, Spain, the United Arab Emirates, the
United Kingdom and the United States. Every pack on the SYSCOHADA chart
names the two accounts a fixed asset leaves the books through,
asset_disposal_value(812) andasset_disposal_proceeds(822), written
once inpacks/ohada/manifest.json. The section is open to every pack, the
way the corporate income tax section is; a company of a country whose pack
does not carry it yet is told so by name,no_fixed_assets_country_rules. -
A member can be removed, and moved to another preset. Membership had a
way in —invite_member(),accept_invitation()— and no way out but a
hand-writtendelete from company_members.remove_member(company, user)
needsmembers.manage, except to leave a company oneself;
set_member_role(company, user, role, capabilities)needs it always and
resets the member's adjustments the wayaccept_invitation()writes a new
member. Both refuse the last owner of a company (last_owner), and both
are recorded by the existingcompany_membersaudit trigger.
company_members_list(company)reads the members with their address from
auth.users, for whoever holdsmembers.manage. The MCP server gains
remove_memberandset_member_role. -
A bank account is not an IBAN, and the European Union is one zone among
others (20260929141500). The installer asked every administrator for an
IBAN, and a company in the United States, the United Kingdom, Australia,
India, Mexico or Japan does not have one. A pack now declares how its banks
identify an account —bank.account_schemeinpack.json, stored in
country_defaults.bank_account_scheme— and one registry in@ekwo-ai/core
reads and checks each scheme:iban,aba-routing-account,
sort-code-account,bsb-account,ifsc-account,clabe,zenginand
transit-institution-account. A pack that declares none is asked for an
account number as the bank wrote it, never for an IBAN.bank_accountsgains
account_schemeandaccount_identifier;ibanstays, is filled for the
ibanscheme and stays empty for every other, and every existing account is
backfilled as the IBAN it was.ekwo init --bank-identifierand the
account_identifierargument ofcreate_bank_accounttake the value (--iban
andibanremain, for IBAN countries only). Every pack that names a scheme
bumped a minor version and itsschema_min.Packs also declare
zones(european-unionfor the twenty-seven Member
States), andekwo pack checkrefuses a zone that has no page under
docs/zones/. The rules that exist only inside the European
Union — intra-community VAT, the recapitulative statement, VIES, the
One-Stop Shop, EN 16931 — are on that page,
beside what the United Kingdom and the
United States packs actually do. Every prose
mention of the Union in the core now says "European Union", and
npm run check:no-bare-eukeeps it so.Upgrading an installation:
ekwo migrateapplies the migration and
re-applies the seeds, which is what writes each country's scheme into
country_defaults. Nothing has to be re-entered, and no company has to be
upgraded: the scheme is read from the country, not copied into the company.
Changed
-
The fixed assets module says which assets it means (
20260929151742,
module version 2.0.0). In accounting English assets is the whole left side
of the balance sheet — stock and receivables included — and this module keeps
only fixed assets: a register, its depreciation and its disposals. Every name
that saidassetsalone now saysfixed_assets:Before From this release schema assetsfixed_assetstable assets.assetsfixed_assets.fixed_assetsassets.create_asset(),assets.dispose_asset()fixed_assets.create_fixed_asset(),fixed_assets.dispose_fixed_asset()constraints, indexes, policies and triggers assets_…fixed_assets_…MCP tools assets_list,assets_create,assets_schedule,assets_run_depreciation,assets_disposefixed_assets_list,fixed_assets_create,fixed_assets_schedule,fixed_assets_run_depreciation,fixed_assets_disposepack section packs/<cc>/assets.jsonpacks/<cc>/fixed_assets.jsonseeds supabase/seed/modules/assets/supabase/seed/modules/fixed_assets/folder modules/assets/modules/fixed-assets/refusal no_assets_country_rulesno_fixed_assets_country_rulesWhat does not change: the module code,
assets. It is the key of
public.modulesand it is written on every entry the module has posted, and
a posted entry does not move. Soekwo module enable assets,
enable_module(company, 'assets')and the capabilitiesassets.read,
assets.writeandassets.poststay, and so does every right a member or a
key holds. Decision 0063
says why.Upgrading.
ekwo migraterenames in place —alter schema … rename,
every row, schedule and posted entry kept to the cent, safe to run twice —
and a fresh installation applies the same migration after the others, so
both reach one catalogue. Then replaceassetswithfixed_assetsin the
exposed schemas of the project (Supabase → Project Settings → API), which
no migration can do;ekwo migratesays so when it has renamed a schema. A
pack of your own renamesassets.jsontofixed_assets.json, and
ekwo pack checknames the file until it is done. French labels are
unchanged: the categories are still « immobilisations ».A company archive written before the rename is taken in as it is: a module
now declares the names its tables were once exported under in
<schema>.archive_former_names(), andimport_company()reads an archive
under the names of today througharchive_under_current_names()
(20260929151700). Only names move; the checksums are over the rows...
Ekwo OS 0.9.0
Added
-
60 more country packs, bringing the set to 107: al, ar, ba, bg, bh, bo, ca, cl, cn, co, cr, cy, cz, dk, do, dz, ec, eg, fi, ge, gh, gr, gt, hr, hu, id, il, in, is, ke, kz, lt, lv, ma, md, me, mk, mt, my, ng, no, om, pa, pe, ph, py, ro, rs, rw, se, si, sk, tn, tr, tz, ua, ug, uy, xk, za. Each
carries its own chart of accounts, its taxes and the return its country
files, as data rather than as code, and each arrives at the status
community— nobody has reviewed it, and the pack says so itself. -
The hosted MCP server is offered to the registry (
remotesin
packages/mcp/server.json):https://mcp.ekwo.ai/mcp, Streamable HTTP. The
npm package is a command to run and the remote is a URL to add — until now
only the first was listed, so a directory had nothing to show somebody whose
assistant went looking for a server. -
AGENTS.mdtells an assistant that an Ekwo account exists, what it is
worth today — a daily backup, and handing an agent the books through one URL
and a browser approval instead of a key in a file — what is coming, and what
is not true: Ekwo does not host the database, andinitstill runs against a
project the person owns. -
The MCP server answers over HTTP too (
handleHttpRequest,
handleNodeRequestin@ekwo-ai/mcp): the Streamable HTTP transport,
stateless and in JSON, for a host that puts the server on the network. The
host authenticates each request its own way and hands in the connection it
may use — a person's access token, or a key of Ekwo OS, which travels in
X-Ekwo-Api-Keywith noAuthorizationbeside it. Aservice_rolekey is
refused in every slot. stdio is unchanged. How to host it behind an OAuth
of one's own is inpackages/mcp/README.md. -
The FEC of two more ledgers is found by the name of the ledger:
ekwo import <name>and the MCP tool read it with the FEC reader, and
docs/compatibility.mdgives the menu each exports it from and its official
page. The FEC reader is tested on the variant they write — separated by
tabs, in UTF-8, not validated yet, with a column after the eighteen. -
A transaction journal saved as CSV is read as books
(@ekwo-ai/transaction-journal,ekwo import transaction-journal). The
report a ledger prints transaction after transaction names no transaction
on its lines: a dated row starts one — or continues it, in a layout that
repeats the date, the type and the number on every line — a row with no
date continues it, and a row with no account, its total, ends it. The
header is found under the title rows by the names of its columns; a date in
digits is read in the order the caller names, and never guessed. The list of
accounts exported beside it gives each account its type. The export of a
named ledger that prints one is listed indocs/compatibility.md. -
An XML Audit File Financial is read as books (
@ekwo-ai/xaf,
ekwo import xaf). Versions 3.2 and 4.0, each found by the namespace of its
schema: the accounts, the parties, the opening balance — posted on the day
the file gives it, so no--opening-dateis asked — and every transaction of
every journal. The totals the file states for its opening and its
transactions are held against its lines, and a file that disagrees with
itself is refused. The XML is read by the strict reader of the statements,
copied; the fixtures are validated against the published schemas. The export
of a named ledger that writes one is listed indocs/compatibility.md.
Changed
- The AI that works on the books is called an agent. The README,
AGENTS.md,
the documentation, the READMEs of the packages, the help ofekwoand of
@ekwo-ai/mcpand the description of the MCP server say "agent" where they
said "assistant". Only the words change: no command, option, variable, tool
or key is renamed. Published decisions and migrations keep their wording.
Fixed
-
list_companiesgives each caller their own role. Two members of a
company see each other's membership rows, and the role shown was whichever
came last; it is now read for the caller alone, and a key of Ekwo OS, which
is nobody's member, is given none. -
An archive survives an honest reader printing it again
(20260923110000). The checksum of a table was the sha256 of the bytes the
database wrote, which is what makesshasumcheck an archive without Ekwo
and is kept. It also meant the checksum answered a question about text, and
anything that stores an archive, streams it, or moves it between two services
parses it and prints it again — which changes the text without changing one
value. That reader was toldarchive_corruptonaudit_log, with the same
row count and another checksum, which reads like a damaged archive and is not
one.One thing changes on that round trip, and only one:
jsonbnormalises key
order and whitespace already, so what differs is numbers nested inside a
jsonb column. PostgreSQL keeps the trailing zeros of1230.00; a JSON
parser hands back1230. The rule that decimals leave as strings was written
for that hazard and reaches thenumericcolumns, which it converts on the
way out; it cannot reach inside ajsonbone, andaudit_log.old_valuesand
new_valuesare full of amounts — so the audit trail failed every time and
the books looked fine beside it.The manifest now carries two checksums per table, because there are two
questions.sha256is the file, byte for byte, andshasumstill checks it;
values_sha256is the same rows in a canonical form —canonical_json()
re-renders every nested number throughtrim_scale()— and that is what
import_company()checks. The data files do not change. A changed value, an
added row, a removed row and a row of another company are refused exactly as
before. An archive written before 0.9.0 carriessha256alone and is checked
against it, with a refusal that now says why its bytes may differ.
docs/company-archive.mdsays what each checksum
guarantees and what it does not.
Ekwo OS 0.8.0
Added
-
A machine key reaches the API (
20260922160000,20260922162500,
decision 0062). A key was for
a client holding a connection:use_api_key()presents it for one
transaction, and PostgREST runs each request in its own, so the key was never
presented when the work ran. Anything operated for somebody else — a backup,
a scheduled report, a function on a host with no psql — had to hold a
password of a real person or theservice_rolekey.The secret now travels in
X-Ekwo-Api-Key, beside the publishable key.
ekwo_pre_request()is called by PostgREST at the start of every request's
transaction (pgrst.db_pre_request), presents the key, and only then moves
the request offanon— which holds no privilege on any table, so the grant
refused before row level security could decide.auth.uid()stays null: a
key is not a session, and what it may do ishas_capability(), as before.A key is now on the company it was minted for, which answers the question
decision 0006 left open by name:is_company_member()says true for that
company, so the rows that describe it — the company row, its members, its
modules, its filing periods, its matching settings, its audit trail — are
readable, exactly as they are to the narrowest member. Nothing of any other
company. The reference tables of the installation askis_known_caller()
where they askedauth.uid() is not null: a signed-in user, or the holder of
a live key.Three limits decision 0006 listed are lifted: a key reads
companies, has a
portfolio of one company, and can export it. An archive is whole or it is not
written, so a key that backs a company up carries the capabilities to read
what the archive carries and not onlycompany.export— the preset a person
exports under isclient, and the refusal names the table and the two
counts.docs/machine-access.mdis the page, with
the curl.The pre-request writes nothing (
20260922171000), which is not a style:
PostgREST opens a GET, and an RPC whose function is not volatile, inside a
read-only transaction, so the stampuse_api_key()puts on
api_keys.last_used_atfailed every read withcannot execute UPDATE in a read-only transaction. The use is now recorded when the transaction may
write and skipped when it may not, solast_used_atis a floor and never a
ceiling — a key read from every night and never written with carries an
old date, or none.ekwo doctorreports whether PostgREST was told to call the pre-request, and
prints the two statements: a managed project may refuse the migration the
right to write the settings ofauthenticator, and a key that is silently
never read is not a thing to leave somebody to discover. -
The version of the schema, asked on its own (
20260922143000).
installed_schema_version()returnsinstance.schema_versionand the
edition, and nothing else of the instance row. The policy oninstance
admits members of at least one company and instance administrators, so a
freshly created account read no row and a client could not tell an
installation behind its schema from one whose row it was never allowed to
see — the same silence, calling for opposite behaviour. Definer andstable.
20260922161500then opened it toanonand moved the decision into the
body: it answers a caller this installation knows — a signed-in user, or the
holder of a live key — and answers anybody else with no rows at all, so the
screen that checks a pasted key can call it and a scanner learns nothing. -
A financial statement asked for by kind (
20260922144500).
financial_statement()takes the code of a scheme, and a code belongs to a
country pack, so a client that did not know the pack could not ask for a
balance sheet at all.default_statement_code(company, kind, at)gives the
one scheme of that kind a company reports on — one its chart declares, else
one of its country, else the generic framework — and
financial_statement_of_kind(company, kind, from, to)answers exactly as
financial_statement()does, with the code it chose in the first column.
The validity is read at the end of the period, so a scheme replaced this
year still prints last year.available_statements()is unchanged and still
lists every scheme a company may ask for.
Changed
-
The schema of this release is 0.8.0, and the packages refuse an older
database by name (20260923093000).ekwo-os,@ekwo-ai/coreand
@ekwo-ai/mcpare built and tested againstekwo_pre_request(),
present_api_key(),is_known_caller(),installed_schema_version(),
default_statement_code()andfinancial_statement_of_kind(), and a 0.7.0
database has none of them — so the floor of the three rises to 0.8.0, and
an installation left behind is named as such rather than failing later in a
query nobody can place.An installation already running takes one command, from anywhere, with
the connection string of its project:npx -y ekwo-os@latest migrate. Then
ekwo doctor, which on this release is not a formality: it is the only
thing that says whether PostgREST was told to call the pre-request. A
managed project may refuse the migration the right to write the settings of
theauthenticatorrole, and a key that is then never read answers nothing
and explains nothing; where that happened,doctorprints the two
statements to run as the owner of the database. Nothing else of the release
asks anything of an existing installation, and what was booked, filed and
closed is untouched. -
The web application has a name: Ekwo Cloud, the hosted edition on
ekwo.ai. Ekwo OS has no screens by design — a database, a command line
and an MCP server — and the README and the Start with Claude guide said
there was no web interface, as if one were missing. They now name the one
there is. Support and every other request go through an Ekwo Cloud account,
athttps://cloud.ekwo.ai/account, where somebody answers, so
DISCLAIMER.mdprints no address. -
The contributor licence agreement is governed by the law of Estonia.
It named Belgian law and the courts of Brussels; Ekwo is operated by Karuna
Co OÜ, a company registered in Estonia, and a dispute falls to the Harju
County Court in Tallinn.CLA.md.
Fixed
isServiceRoleKey()read no key in a browser. It decoded the payload of
a JWT withBufferinside atry. A browser has noBuffer: the call
raised, thecatchswallowed it, and the function answered this is not a
service_role key about every JWT it was ever shown — the one answer that is
never safe to guess, in the guard that exists so no surface acts with a key
that bypasses row level security. The payload is now decoded with base64url
written out andTextDecoder, which Node and every browser carry, and a key
shaped like a JWT whose payload cannot be read is refused rather than
cleared. The shape is read off the header — a JSON object naming its
algorithm — so a connection string or a password with two dots in it is
still not a token.