Skip to content

Releases: Ekwo-ai/ekwo-os

Ekwo OS v0.16.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 19 Oct 23:08

Added

  • The crm module: the record of a relationship, kept on the socle's
    contacts
    (module migration 20261019175503). 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 calls upsert_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_of and search_cards. Row level security through
    module_enabled() and the module's own crm.read and crm.write; the
    company archive carries every table of crm; 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-pdf prints 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_url per environment; a validation recorded
    without a link gets it built, or is refused with
    clearance_verification_unbuilt. ekwo einvoice clearance takes
    --environment, einvoicing_clearance takes environment.
  • A line carries what its electronic invoice format asks (socle
    migration 20261019170000). products.attributes and
    document_lines.attributes hold 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_product and update_product take
    attributes; --line takes attr.<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/myinvois writes TaxCurrencyCode and every tax amount in
    it.
  • A free Ekwo Cloud account is offered once, at the moment it helps. One
    line after ekwo init and after a first company 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, with EKWO_NO_CLOUD_OFFER, or once
    settings.json says "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.my in
    production and preprod.myinvois.hasil.gov.my in the sandbox, from the
    SDK FAQ, now a source of the pack. Every tax under E carries the
    exemption_reason the 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/core reads document_lines.attributes and
    products.attributes to write an electronic invoice, and @ekwo-ai/mcp
    writes them, neither of which a 0.15.0 database has. An installation runs
    ekwo migrate before upgrading the packages. The end-to-end run against a
    real Supabase project was not run for this release.

Ekwo OS v0.15.1

Choose a tag to compare

@ekwo-ai ekwo-ai released this 19 Oct 14:58

Changed

  • The schema stays 0.15.0, and so does the floor of the packages. This
    release redefines suggest_duplicate_contacts(), filing_tax_movements()
    and prepare_filing() and fills tax_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 runs ekwo migrate for 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) and contact_candidates() are unchanged.
  • A return settles the tax of its own form. filing_tax_movements() —
    what settle_filing() clears and lock_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_date from filing_deadline() — the pack's
    deadline.rule — and fills it on a draft that has none; the filings
    already written get theirs. The debt settle_filing() posts matures on it.
    A pack with no rule, or depends_on_taxpayer, leaves it empty.
  • Malaysia 0.3.1. The README says that settling SST-02 leaves 2102, the
    self-assessed Service Tax of MY-P-SVT-IMPORT, for form SST-02A, which the
    pack does not carry, and how to clear it with one entry. schema_min
    rises to 20261019161437.

Ekwo OS v0.15.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 19 Oct 11:50

Added

  • Fiscalisation by clearance: the invoice the authority validates before it
    is issued.
    A third regime of einvoicing.fiscalisation, beside the
    chained record and the report, modelled on what MyInvois, KSeF and SdI
    share: a clearance object, exclusive of hash, 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
    clearance once normalised). For an obligation phased by size, each
    mandatory_from entry may carry turnover_above (an amount and its
    currency) and its own text, and exemption lists the dated thresholds below
    which a taxpayer is exempted, oldest first; ekwo pack check refuses 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), keeps einvoicing.clearances — one submission per
    attempt, submitted, validated (identifier, moment, verification link),
    rejected (by the authority or the buyer, with the reason) or cancelled —
    with record_clearance_submission() and record_clearance_answer(), which
    raise on every transition that cannot have happened and on a window that
    has closed, and append each answer verbatim to fiscal_events. Neither
    calls the authority: the transport is the host's. recordClearanceSubmission()
    / recordClearanceAnswer() back ekwo einvoice clearance and 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 format myinvois, 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 now myinvois, the document
    submitted to the MyInvois API, in place of pint-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 gains ekwo line contacts suggest and ekwo 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 and community: 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
    under packages/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, pack my 0.3.0). myinvois is registered in
    EINVOICE_FORMATS, so ekwo einvoice validate and issue,
    einvoicing_validate and einvoicing_issue write the document of a
    posted sale through @ekwo-ai/myinvois, version 1.1 by default, in its
    JSON syntax, and fiscalisationStatus() 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.fiscalisation gives 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.identifiers and contacts.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_description the words of its activity beside its
      code; document_header publishes them as seller_identifiers,
      buyer_identifiers, seller_activity_description, with buyer_phone;
    • the moment of issuance is the moment the file is written, or issued_at
      of validateEinvoice() and issueEinvoice(); 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_contact takes registration_number, phone and identifiers
      (and ekwo contact add --stdin the same fields); update_company_profile
      takes identifiers and activity_description.
  • Two contacts of one company are merged (socle migration
    20261019090000, module assets migration 20261019090001, decision 0074).
    merge_contacts(keep, absorb) moves every reference of the absorbed contact
    onto the kept one in one transaction — every foreign key to contacts, 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 asks contacts.write and
    reconcile.write, and refuses unknown_contact, same_contact,
    already_merged, different_identifiers (two VAT or registration
    numbers) and matching_across_parties by 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_id move; the trail gets contacts_merged, line_contact_merged
    and document_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 gains ekwo 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) and because.
    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 into name_agreement() and
    name_among_several(), the comparison of an account into account_key()
    and of a number into party_identifier(), read by contact_candidates()
    and merge_contacts() alike. About 40 ms at 2 000 contacts. The command
    line gains ekwo contact duplicates and the MCP server
    suggest_duplicate_contacts.

  • An analytic value is entered on the line, and read from the ledger
    (socle migration 20261019130000, 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_group and 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). Its amount is
    derived by the database and a whole split adds up to its line to the cent.
    products.analytic_value_id and contacts.analytic_value_id pre...

Read more

Ekwo OS v0.14.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 18 Oct 15:44

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() sets fiscal_years.finally_closed_at on a
    closed year, after every closed year before it; from then on
    reopen_fiscal_year() refuses it (fiscal_year_finally_closed) and the
    guard fiscal_years_guard_final refuses any change to the year, for
    everybody, service_role and 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 by fiscal_year_final_close(). The
    French pack (1.21.0) declares it under the PCG, art. 921-4. The MCP server
    gains close_fiscal_year_finally, and close_fiscal_year answers 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 trigger attachments_guard_document refuses a file on an
    entry that a document posted, with attachment_belongs_on_document naming
    the document to attach to instead; an entry no document posted keeps its own
    pieces. docs/import.md says how a taken-over history brings its invoices
    in as documents, with their PDFs.

  • Fiscalisation, a capability of the einvoicing module. A country pack
    may declare einvoicing.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) and pack describe / describe_pack show it. The module,
    version 1.1.0 (20261018015212), keeps einvoicing.fiscal_chain and
    einvoicing.fiscal_events, immutable, with append_fiscal_record(),
    verify_fiscal_chain() and log_fiscal_event(); a record's file is an
    issue of artefact fiscal_record. The core registers formats in
    FISCALISATION_FORMATS, with @ekwo-ai/verifactu first, and
    fiscaliseDocument() / fiscalisationStatus() back ekwo einvoice fiscalise / fiscalisation and the MCP tools einvoicing_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.
    In einvoicing.fiscalisation, hash becomes optional: left
    out, the regime reports the documents' data to the authority and nothing is
    chained (kind reporting once normalised). announced_from, with
    announced_by and announced_on, carries a day announced while no text is
    in force, exclusive of mandatory_from; submission.channel and
    submission.cadence (per category of taxpayer and kind of data: the period
    and the due rule) carry a batch sent by period; reported names what is
    reported and by whom. Socle migration 20261018023930 widens the shape
    check of country_defaults.einvoice_fiscalisation. fiscaliseDocument()
    refuses a reporting regime by name (format_without_brick, then
    fiscal_regime_without_chain), and fiscalisationStatus() 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) (migration 20261016145816) 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 says active
    false. Security invoker, so the caller reads what documents.read lets
    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, and document_lines.product_id is the one place that does.

  • Every company gets an analysis, whatever its country. A company whose
    country pack carries no analysis.json used 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, as packs/generic/statements.json builds 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
    20261016065440 lets analysis_templates and analysis_rules hold a
    framework's rows, with no country and no chart, and financial_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, pack or framework, typed FinancialAnalysisRow.source in
    @ekwo-ai/core and explained by the financial_analysis tool 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 and community: 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/mcp calls close_fiscal_year_finally() and
    fiscal_year_final_close() and reads country_defaults.einvoice_fiscalisation,
    and the packs ekwo-os carries write einvoice_fiscalisation,
    final_close_due and the framework of 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/core and @ekwo-ai/mcp rises to 0.14.0 and an
    installation left behind is told to run ekwo 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 ee 1.13.0 adds
    analysis.json: the sixteen aggregates financial_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 proven on the real books of a company. Pack ee
    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.md 2.2 names Ekwo Consulting as
    a trade name of EKWO OÜ beside Ekwo. Nothing in the schema or the p...

Read more

Ekwo OS 0.12.2

Choose a tag to compare

@ekwo-ai ekwo-ai released this 13 Oct 18:13

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 new result_in_progress of statements.json, whether the
    line that receives the result is the result alone (France DI, 51
    schemes) or combined with other balances (Belgium 14, every scheme that
    closes into retained earnings, the SYSCOHADA CJ; 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_progress under that line; financial_statement(), the
    totals and every filing are unchanged. statement_account_matches() gains
    the column in_progress, and statement_figures() is the one place a
    statement is summed. ekwo pack check refuses a balance sheet that leaves
    the rule out or contradicts its own scheme and chart. Migration
    20261011120000, which every pack now names as its schema_min.

  • The corporate income tax module speaks to an agent. Eight MCP tools
    under the tax prefix, offered once the module is installed:
    tax_estimate (the year line by line, each line with its article),
    tax_parameters and tax_declare_parameter (the facts the country asks a
    company to declare, written only as the user states them), tax_adjustments
    and tax_declare_adjustment (the rules in force and what the company names
    under each: an account or an amount), tax_losses,
    tax_record_computation and tax_computations. The prompt
    tax_review_year walks 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
    (module tax 1.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, through post_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 three result_in_progress columns
    of statement_templates and the balance sheet that prints them, and the
    four integrity fixes below. Every pack ekwo-os carries writes those
    columns when it is installed or upgraded, which a 0.12.1 database does not
    have, so the floor of ekwo-os, @ekwo-ai/core and @ekwo-ai/mcp rises to
    0.12.2 and an installation left behind is told to run ekwo 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 under journal_reasons, which ekwo import
    shows.

  • The French chart carries the capital called and paid, the tax withheld at
    source and the research tax credit
    (pack fr 1.17.2): 101300 (PCG
    1013), 442 and 442100 (PCG 4421), 699 and 699100 for 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 of 699100 out 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_id and state were plain columns under
    bank.write: setting entry_id back to null let the same money be settled
    a second time, writing reconciled took 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
    between pending and ignored, and leaves reconciled and entry_id to
    settle_from_statement(), which raises ekwo.settling_statement around its
    one update, honoured for a caller holding reconcile.write
    (bank_transaction_settled_by_rpc otherwise, 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 read contact_id, so on a receivable account shared by every
    customer the payment of one could settle the invoice of another; and
    reconciliations was writable by hand under reconcile.write, past every
    check reconcile() 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 to reconcile() and unreconcile(), which raise
    ekwo.reconciling, honoured for a caller holding reconcile.write
    (reconciliation_by_reconcile otherwise). 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 any declaration_box and box_amount, with no
    tax, and vat_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 — a tax posting for a
    tax line, base or tax_on_base otherwise
    (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's tax_postings, which a
    member with settings.write edits and pack_upgrade() rewrites, so a
    return already filed for a locked period could come out with other
    figures. Two columns, entry_lines.declaration_boxes and
    entry_lines.report_code, are now frozen when the line is written, from
    the posting that writes it, by entry_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 one vat_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

Choose a tag to compare

@ekwo-ai ekwo-ai released this 11 Oct 10:23

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 new not_taxed of
    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 check now refuses a pack whose taxes include a rated purchase VAT,
    GST or sales tax and which does neither, and docs/packs.md says how to
    write either.

  • An invoice wears the colour of its company. Two nullable columns on
    companies, invoice_accent_color (#rrggbb) and invoice_logo_position
    (left or right), checked by the database, written by
    update_company_profile with company.write and read back by
    get_company. ekwo doc pdf and render_invoice_pdf read them and hand
    them to @ekwo-ai/invoice-pdf as 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 adds year_result_account() and
    statement_rule_catches(), changes what a balance sheet reads, and carries
    the two invoice columns of companies. get_company, ekwo doc pdf and
    render_invoice_pdf read those columns, which a 0.12.0 database does not
    have, so the floor of ekwo-os, @ekwo-ai/core and @ekwo-ai/mcp rises to
    0.12.1 and an installation left behind is told to run ekwo 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 (DI in France), retained earnings otherwise
    (rubric 14 in 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 is year_result_account(),
    which fec_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 asked filings.read, and the two files they name — what was
    sent and the receipt — asked documents.read, like every attachment: a
    member whose filings.read was withdrawn and who kept documents.read
    read them, within their own company. An attachment of a declaration, or
    one a deposit names, is now read with filings.read and written with
    filings.write, by a person and by a machine key alike; every other
    attachment keeps documents.read and documents.write. The policy finds
    the deposit through attachment_is_filing_deposit_file(), a definer
    function, because the deposits are hidden from precisely the member this
    is about.

Ekwo OS 0.12.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 10 Oct 18:55

Added

  • The privacy page says the agents of Ekwo Cloud may learn from the books.
    Version 1.3 of PRIVACY.md adds 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.md opens 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 optional ekwo register and 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 dates Intl'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 tool render_invoice_pdf read 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-x now 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-ubl reads a received Peppol BIS Billing 3.0 invoice or
    credit note (UBL 2.1); readCii() and readFacturX() of
    @ekwo-ai/factur-x read a Factur-X or ZUGFeRD invoice of any profile, from
    its XML or out of its PDF. Both return the same ReceivedInvoice — 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 in violations, never corrected; a
    file that is not an invoice throws an InvoiceFileError with 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 einvoicing module: 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-3 by @ekwo-ai/peppol-ubl,
    factur-x-en16931 by @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. Capabilities einvoicing.read and
    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, receive and lookup, 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
    and einvoicing_list
    , and ekwo 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_status and peppol_message_id
    carry its state and reference, which is what keeps unpost_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_proofs keeps 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() and upgrade_filing_proof() are
    the only way in and need the new capability filings.prove, on owner and
    accountant. A complete proof is frozen. tax_filing_values_sha256() hashes
    the frozen figures of a declaration with canonical_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/core reads and writes the .ots format, 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) and ekwo proof verify <file> [--ots] [--out].

    The method column also accepts eas, the Ethereum Attestation Service.
    Nothing writes it yet: docs/filing-proofs.md evaluates 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/ar 0.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
    new AR-P-EXT (foreign_services_received, 21 %) puts the base in an
    informational box PCEXT, debits the tax to a new 1153, a credit not yet
    computable, and credits it to a new 2135, the tax to pay, with no box of
    crédito fiscal in the month of the invoice. Moving the credit to 1151 in
    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 on 2112. 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/core and ekwo proof
    call record_filing_proof() and upgrade_filing_proof(), which a 0.11.1
    database does not have — so the floor of ekwo-os, @ekwo-ai/core and
    @ekwo-ai/mcp rises to 0.12.0, and an installation left behind is told to
    run ekwo migrate rather than failing in a call nobody can place.

  • The privacy page says when Ekwo reads your books. Version 1.1 of
    PRIVACY.md replaces 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...

Read more

Ekwo OS 0.10.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 02 Oct 08:01

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 — in tax.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 in tax.adjustments, by account or by
    amount, and the losses it carried in in tax.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. Add tax to 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 in packs/schema/pack.1.json and in docs/packs.md, compiled
    into supabase/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 with golden/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 check refuses 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.json gives 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) and asset_disposal_proceeds (822), written
    once in packs/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-written delete from company_members. remove_member(company, user)
    needs members.manage, except to leave a company oneself;
    set_member_role(company, user, role, capabilities) needs it always and
    resets the member's adjustments the way accept_invitation() writes a new
    member. Both refuse the last owner of a company (last_owner), and both
    are recorded by the existing company_members audit trigger.
    company_members_list(company) reads the members with their address from
    auth.users, for whoever holds members.manage. The MCP server gains
    remove_member and set_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_scheme in pack.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, zengin and
    transit-institution-account. A pack that declares none is asked for an
    account number as the bank wrote it, never for an IBAN. bank_accounts gains
    account_scheme and account_identifier; iban stays, is filled for the
    iban scheme and stays empty for every other, and every existing account is
    backfilled as the IBAN it was. ekwo init --bank-identifier and the
    account_identifier argument of create_bank_account take the value (--iban
    and iban remain, for IBAN countries only). Every pack that names a scheme
    bumped a minor version and its schema_min.

    Packs also declare zones (european-union for the twenty-seven Member
    States), and ekwo pack check refuses 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-eu keeps it so.

    Upgrading an installation: ekwo migrate applies 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 said assets alone now says fixed_assets:

    Before From this release
    schema assets fixed_assets
    table assets.assets fixed_assets.fixed_assets
    assets.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_dispose fixed_assets_list, fixed_assets_create, fixed_assets_schedule, fixed_assets_run_depreciation, fixed_assets_dispose
    pack section packs/<cc>/assets.json packs/<cc>/fixed_assets.json
    seeds supabase/seed/modules/assets/ supabase/seed/modules/fixed_assets/
    folder modules/assets/ modules/fixed-assets/
    refusal no_assets_country_rules no_fixed_assets_country_rules

    What does not change: the module code, assets. It is the key of
    public.modules and it is written on every entry the module has posted, and
    a posted entry does not move. So ekwo module enable assets,
    enable_module(company, 'assets') and the capabilities assets.read,
    assets.write and assets.post stay, and so does every right a member or a
    key holds. Decision 0063
    says why.

    Upgrading. ekwo migrate renames 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 replace assets with fixed_assets in the
    exposed schemas of the project
    (Supabase → Project Settings → API), which
    no migration can do; ekwo migrate says so when it has renamed a schema. A
    pack of your own renames assets.json to fixed_assets.json, and
    ekwo pack check names 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(), and import_company() reads an archive
    under the names of today through archive_under_current_names()
    (20260929151700). Only names move; the checksums are over the rows...

Read more

Ekwo OS 0.9.0

Choose a tag to compare

@ekwo-ai ekwo-ai released this 02 Oct 08:01

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 (remotes in
    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.md tells 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, and init still runs against a
    project the person owns.

  • The MCP server answers over HTTP too (handleHttpRequest,
    handleNodeRequest in @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-Key with no Authorization beside it. A service_role key is
    refused in every slot. stdio is unchanged. How to host it behind an OAuth
    of one's own is in packages/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.md gives 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 in docs/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-date is 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 in docs/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 of ekwo and of
    @ekwo-ai/mcp and 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_companies gives 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 makes shasum check 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 told archive_corrupt on audit_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: jsonb normalises key
    order and whitespace already, so what differs is numbers nested inside a
    jsonb column
    . PostgreSQL keeps the trailing zeros of 1230.00; a JSON
    parser hands back 1230. The rule that decimals leave as strings was written
    for that hazard and reaches the numeric columns, which it converts on the
    way out; it cannot reach inside a jsonb one, and audit_log.old_values and
    new_values are 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. sha256 is the file, byte for byte, and shasum still checks it;
    values_sha256 is the same rows in a canonical form — canonical_json()
    re-renders every nested number through trim_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 carries sha256 alone and is checked
    against it, with a refusal that now says why its bytes may differ.
    docs/company-archive.md says what each checksum
    guarantees and what it does not.

Ekwo OS 0.8.0

Choose a tag to compare

@ekwo-team ekwo-team released this 23 Sep 04:15

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 the service_role key.

    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 off anon — 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 is has_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 ask is_known_caller()
    where they asked auth.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 only company.export — the preset a person
    exports under is client, and the refusal names the table and the two
    counts. docs/machine-access.md is 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 stamp use_api_key() puts on
    api_keys.last_used_at failed every read with cannot execute UPDATE in a read-only transaction. The use is now recorded when the transaction may
    write and skipped when it may not, so last_used_at is a floor and never a
    ceiling — a key read from every night and never written with carries an
    old date, or none.

    ekwo doctor reports 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 of authenticator, 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() returns instance.schema_version and the
    edition, and nothing else of the instance row. The policy on instance
    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 and stable.
    20260922161500 then opened it to anon and 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/core and
    @ekwo-ai/mcp are built and tested against ekwo_pre_request(),
    present_api_key(), is_known_caller(), installed_schema_version(),
    default_statement_code() and financial_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
    the authenticator role, and a key that is then never read answers nothing
    and explains nothing; where that happened, doctor prints 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,
    at https://cloud.ekwo.ai/account, where somebody answers, so
    DISCLAIMER.md prints 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 with Buffer inside a try. A browser has no Buffer: the call
    raised, the catch swallowed 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 and TextDecoder, 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.