Repository navigation
Pinakes v0.9.0-rc.1
Pre-release
Pre-release
·
66 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Release candidate: published as a GitHub prerelease, offered only to installations that opt in with UPDATER_CHANNEL=rc or UPDATER_ALLOW_PRERELEASE=1. Every new module can be switched off in Settings → Modules and Settings → Loans; an upgraded installation keeps its settings, and stocktake, suppliers, the visitor log, RDA overrides and fines start switched off there (a library that already used one of them on an earlier 0.9.0 build keeps it on).
Inventory stocktake
- Stocktake sessions (Admin → Revisione inventariale,
/admin/inventory). A session is opened with a name, an optional branch and a filter (bookcase, shelf level, genre with its subgenres, media type, collocation range). Opening it freezes the list of expected copies ininventario_attese, books removed from the catalogue excluded, so moving a book, adding or deleting a copy while the stocktake runs does not change what the session looks for. Copies that are not on any shelf when the session opens (lost, damaged, being restored, in transit, in maintenance) are left out of the expected set, so they are not reported missing for the whole session; lent copies and copies waiting for pickup stay in, with a bucket of their own. - Scanning works with keyboard-wedge barcode readers (the code field keeps the focus, Enter records), with the self-hosted camera scanner already used at the loan desk, and with an uploaded
.txt(one code per line, the whole line is the code) or.csv(first column, read with a CSV parser) list. Each read is classified as found, misplaced (found on a bookcase other than the expected one, as declared by "Scaffale in cui stai lavorando"), on loan or awaiting pickup, unknown code, out of scope or repeated, with its own colour and sound. Several operators can scan the same session at once: the counters refresh every few seconds and "Solo le mie scansioni" narrows the list to one's own reads. - Live report: expected copies always add up as found + misplaced + on loan (lent, or set aside on the pickup shelf for a reader) + missing; unknown, out-of-scope and repeated reads are counted on the side. PDF and CSV reports are available while the session is open and after it ends. The report of a closed session is frozen at close: the bucket of every expected copy is written to
inventario_attese.esito_chiusura(found, misplaced, on loan, missing, or lost when the close marked it so), and a loan or a return after the close no longer moves a copy between buckets in the page, the counters or the exports. - Closing either leaves the missing copies as they are or marks them
perso. Marking them lost is an administrator action, checked on the server: Staff and custom roles can scan and close a session, but not declare copies lost. Copies on loan or waiting to be picked up (by their state or by an active loan row), damaged, being restored, in transit, in maintenance or already lost are never touched; holds on a copy declared lost move to another copy as with a manual change of state; book availability is recalculated throughDataIntegrity, and each copy gets a "Copia dichiarata persa in revisione inventariale" entry in the book's activity log. The close records its choice (inventario_sessioni.azione_mancanti): when a mark-lost close did not reach every copy (the request died, or a book failed), an administrator completes it from the closed session's report, and running it again is harmless. A session can also be cancelled without changing any copy. - Permission
inventory.managefor custom roles, and a new starting profile "Revisore inventario" (backend access + stocktakes). Staff and administrators have it already.
MARC bulk import, export and copy cataloguing
- Shared MARC library in core (
app/Support/Marc/). One neutralMarcRecord, streaming ISO 2709 and MARCXML readers and writers written in house (no new dependency), MARC-8 to UTF-8 for Basic and Extended Latin (ANSEL spacing letters and combining diacritics), format detection, and mappers from MARC 21, UNIMARC and danMARC2 to book data. A malformed record, or one in a MARC-8 character set that is not Latin, is reported on its own and the rest of the file goes on. The OAI-PMH server builds its MARCXML, UNIMARC and ISO 2709 records with it (golden files pin the output byte for byte; compared with 0.8.9 the records gain 007, the RDA 336-338 fields and leader/06 by media type, the genre moves from 650 to 655$2 local, every language gets its own 041$aand HTML entities in the summary are decoded), and the z39-server plugin's role reader, danMARC2 reader and UNIMARC reader now live there. - Bulk MARC import (Books → Import → MARC,
/admin/books/import/marc). A.mrc/.iso(ISO 2709) or.xml(MARCXML) file is read once, a preview of the first ten mapped records says which are already in the catalogue, and the import runs in chunks with a progress bar through the CSV import's own row pipeline: authors and publishers are found with the exact-spelling rules of #501, one copy per book, the import history and its error download work as for CSV. A record matching a book by ISBN-13, ISBN-10 or EAN (deleted books have them cleared, so they never match) is skipped, completed in its empty fields, or overwritten with what the record carries. Covers come from 856. Holdings (852/949/952) are not turned into copies. - Bulk MARC export (Books → Export, and "Esporta in MARC" on a selection). The catalogue or the selected books as MARC 21
.mrc, a MARCXML collection or UNIMARC.mrc, written while the download is sent, so a large catalogue never sits in memory. Deleted books and desiderata requests are never exported. The records carry the EAN, the LC classification, the Dewey number and the cover, so an export re-imports without duplicates whenever the import can recognise the book: by ISBN-13, ISBN-10 or EAN, or else by title and first author. A book with none of these (a title alone) has nothing to match on and is imported again as a new book. - Copy cataloguing (Books → Import → Copy cataloging,
/admin/books/copy-cataloging, z39-server plugin). Search by title, author, ISBN/ISSN or all fields in the configured SRU servers, SBN, the Library of Congress, BnF, DNB, K10plus and this installation's own SRU server; look at each record's MARC fields; import the chosen ones with the same duplicate modes and cover download.SruClient::search()returns every record of a query. Another Pinakes installation is a preset in the plugin settings, and its SRU server's UNIMARC records now carry the cover in 856 (the MARCXML ones already did). - The media type travels in MARC. A book's media type (libro, disco, audiolibro, dvd, altro) sets leader/06 (a, j, i, g), the 007 category of material and the RDA content, media and carrier types in 336/337/338 (
$aterm,$bcode,$2vocabulary; a book's own RDA codes, when it has them, override the implied ones); UNIMARC records set label/06 the same way. On import the media type is read from leader/06 (a sound recording of performed music is a disco, of spoken word an audiolibro) and RDA codes the media type does not imply are kept as the book's own, once the catalogue has the columns for them; in overwrite mode they replace those of a book already in the catalogue (a DVD record matched to a printed book makes it a DVD), while its format and price are never read from a record. OAI-PMH MARC 21 records now carry leader/06 by media type and RDA 336-338 (the golden files are regenerated). - The genre survives a MARC round trip. A MARC 21 record carries the genre as 655 (ind2 7,
$2 local), the keywords as 650; before, both were 650 and the genre came back as a keyword. On import a 655 marked$2 localbecomes the book's genre (created when the catalogue lacks it, as the CSV import does); form headings of other schemes (LCGFT, RBMS) are left alone. In fill mode a book without a genre gets the record's. OAI-PMH MARC 21 records emit the genre as 655 too. UNIMARC does the same with its 606: the genre carries$2 local(in the bulk export and in OAI-PMH UNIMARC records), and the UNIMARC import reads that 606 as the genre and the other 606 as keywords. OAI-PMH MARCXML records now declarexmlns:xsion the record, as the oai_dc and UNIMARC ones already did, so a record lifted out of the envelope is a valid document. - Corporate and meeting authors are read from MARC 21 110/111 and 710/711 (
$aand$bjoined), with the same roles as personal names; 082 prime marks ("891.73/3") are dropped from the Dewey number. - Fill mode completes a family as soon as one of its columns is empty (an ISBN-10 next to an existing ISBN-13, a series number next to its series) and never overwrites a field the book has.
- Smaller MARC 21 corrections. A book in several languages exports one 041
$aper language and an import joins every 041$a("English, Italiano"); an unknown language code is no language rather than a raw code in the field. A monograph's series ISSN (490/830$x) is read when 022 is absent. "2 v." or "24 cm" in 300 is no page count. The number and name of a part (245$n$p, UNIMARC 200$h$i) are appended to the title. A field over the 9,999-byte ISO 2709 field limit is cut at the last subfield boundary that fits and the cut is logged with the book id; the UNIMARC.mrcdownload of the OAI-PMH plugin shortens a record over 99,999 bytes as the bulk export does. The copy cataloguing presets usebib.fuzzyIsbnfor the BnF (either ISBN form) andwoe allfor the DNB's all-fields search, a server configuration may name the relation of an index, and a per-record diagnostic in an SRU answer (the BnF's "erreur de traitement") skips that record instead of the whole search.
Minor features (visitor log, catalogue cards, suppliers, RDA types, statistics)
- Visitor log (
/admin/attendance, sidebar "Presenze"). A kiosk page records each visit: a member scans the library card (or the desk types the code), a guest is recorded with an optional name and reason, and an optional branch when the installation has more than one. The page shows today's visits, visits per day and per hour slot over 7/30/90/365 days, the latest visits with members' names shortened to "Name S.", and a CSV export of the daily counts. New tablepresenze. - GDPR retention for the visitor log. Visits are kept for 365 days by default; an administrator can change the number of days (0 keeps them forever) and apply it immediately. The scheduled maintenance (
cron/full-maintenance.php) deletes older visits on every run. Deleting a member keeps the visit, anonymised. - ISBD catalogue cards. "Scheda catalografica" on the book page and "Schede catalografiche" on a selection in the book list produce a PDF of 125 × 75 mm cards, one per book: call number, main entry heading (principal author, surname first), ISBD description, standard number and subject tracings. Deleted books are never printed.
- Suppliers (
/admin/suppliers, sidebar "Fornitori"). New tablefornitoriwith name, contact, VAT number, website, address and notes; a supplier select in the book form ("Dettagli Acquisizione"), the supplier on the book page and a "Fornitore" filter in the book list. Deleting a supplier keeps its books and clears their supplier (libri.fornitore_id, ON DELETE SET NULL). An e-mail or website that is not one is refused by the supplier form with a message, never saved as empty. The MARC and CSV import pages have a "Fornitore" select: the chosen supplier goes on the books the import creates and, for a MARC import in "Sovrascrivi" mode, on the books it overwrites; skipped duplicates and books only completed in their empty fields keep theirs. - RDA content, media and carrier types in cataloguing. The book form shows the three RDA types derived from the media type and lets the cataloguer override each one for a single record (
libri.rda_content_type,rda_media_type,rda_carrier_type, NULL = derived). The book page shows the effective types. The BIBFRAME output carries the same effective types: books keep txt/n/nc as before; DVD, disc and audiobook records now emit their real RDA types (tdi/v/vd, prm/s/sd, spw/s/sd) instead of a book's; an override wins over the derived value. - Statistics.
/admin/statisticsadds loans of the last 12 months by material and the visitors of the last 30 days. The outcomes of the last closed stocktake (expected, found, misplaced, on loan, missing, unknown codes) appear once a session has been closed. Loans of the last 12 months by patron type (readers without a type under the default type) come with the circulation policy; fines have their place on the page and appear as soon as that feature provides data (App\Support\StatisticsSections).
Library cards, patron photo and user import
- Printable library cards. Every reader's page has "Stampa tessera": a TCPDF PDF in CR80 size (85.6 × 54 mm, one card per page) or on A4 sheets of business-card stock with cut lines (10 per sheet, 9 for the portrait layout). The front carries the library logo and name, the reader's name, the card code as a Code128 barcode or a QR code, the photo and the expiry date; the back carries a text of your choice (rules, website, opening hours), mirrored on the A4 sheet so a long-edge duplex print puts each back behind its own card. The users list gained a selection column: selected readers print in one PDF, as single cards or A4 sheets.
- Settings → Tessere (
/admin/settings/cards): three layouts (classic with a header band, minimal without photo, portrait), Code128 or QR, default format, header text, which elements to show, back text. Untouched settings give the classic Code128 card; nothing else changes for an installation that never prints a card. - Patron photo from the webcam. The user form has "Scatta foto" (the browser camera, cropped to a portrait frame on a canvas) and "Carica foto"; both go to the same endpoint, which re-encodes the image as a plain JPEG of at most 600 px without EXIF. Photos live in
storage/patron_photos/, outside the web root, and reach a browser only through/admin/users/{id}/photobehind the same permission as the reader record. "Elimina foto" removes file and reference at once (the reader's right to erasure), and deleting an account deletes its photo, also when the account has a loan history and is therefore only suspended.Permissions-Policykeepscamera=(self)(now built byContentSecurityPolicy::permissionsPolicy()); the CSP needed no change. - User import from CSV (
/admin/users/import, "Importa CSV" in the users list). Upload, preview of the first rows, column mapping with automatic recognition of Italian, English, German, French and Danish headers (accented letters included, whatever the server's locale) and of the custom registration fields, then chunked processing with a progress bar. Rows whose email or card code already exists (or already appeared in the file) are skipped as duplicates and never merged; invalid rows are reported with their line number. Passwords are never imported: an optional checkbox sends each new reader the password-setup invitation (PasswordSetupToken). Every run is written to the import history (typeusers) with the errors downloadable as CSV. A file that is not UTF-8 (a "CSV" saved by Excel on Windows is in the system code page) is converted from Windows-1252 on upload and the preview says so, instead of the upload failing with an empty response. A patron-type column ("Tipologia") assigns the type by its code or, failing that, by its name (both in any case; a name shared by two types is not guessed), so the users export of another installation with the same types re-imports cleanly; an empty value leaves the reader on the default type, and so does an unknown or deactivated one, reported as a warning with its line. The card expiry follows the user form: when the file gives none, it is computed from the card duration of the assigned patron type (the default type when the row has no type), and an expiry present in the CSV always wins. The separator (";", "," or TAB) is the one that splits the first lines into the same number of columns as the header, quoted fields respected, so a ";" file whose addresses hold commas is no longer read as a one-column "," file.
Circulation policy
- One source for the circulation rules. Loan duration, request window, renewals and the active-loan limit were read straight from the loans settings in about twenty places (staff desk, reader requests on the website and in the app, approval, queue promotion, renewals, NCIP). They now all go through
App\Services\CirculationPolicy, which returns immutableUserPolicy/LoanPolicyobjects, and every due date is computed byCirculationPolicy::dueDateFor(). With only the global settings the numbers are the same as before (a unit test compares every old read site over a grid of stored values); a CI step rejects any new direct read of those settings. - Patron types (Settings → Circulation). Every reader belongs to a patron type (child, student, teacher…) that decides the circulation rules; the role (
tipo_utente) keeps deciding only the permissions. A type can set the total active-loan limit, loan duration, longest request, renewals and days per renewal, whether reservations are allowed and how many, and the card duration; an empty field inherits the global loans setting. Types are created, edited, ordered and deleted from the new tab, one of them is the default for patrons without a type. The user forms have a "Tipologia lettore" select, the reader page shows the effective rules, the user list filters by type and the CSV export has the column, followed by the type's code ("Codice tipologia lettore") so a round trip through the user import is exact even when two types share a name. Existing users are not migrated: everyone starts on the default "Standard" type, which inherits everything. - Rules matrix patron type × material (
libri.tipo_media). A cell can make a material consultation only, cap the loans of that material on top of the total limit, and set its duration, renewals and days per renewal; the matrix shows the inherited value of every empty cell in grey. The book page's renew button and its confirmation say the real renewal length of the loan instead of a fixed "+14 giorni". Per field, the first value wins among rule (type, material) → rule (type, every material) → rule (every type, material) → patron type → global settings. - Consultation-only copies. A single copy can be kept for in-house consultation from the book page (
copie.solo_consultazione): it is never allocated to a loan nor counted as lendable capacity, and a title whose every copy is kept for consultation is consultation only. The book page shows "Solo consultazione" instead of the request button. The availability counterlibri.copie_disponibili(book page button and badge, catalogue "available" filter, OPAC and mobile availability) is recalculated without the consultation copies, so a title whose only copy on the shelf is a consultation copy offers "Prenota quando disponibile", not "Richiedi prestito";copie_totalikeeps counting every physical copy in circulation. Existing installations get the counters recalculated by the upgrade (the Updater re-derives availability after the migrations) or from Maintenance → "Ricalcola disponibilità". - The same rules on every channel. Staff desk, reader requests, approval, queue promotion, renewals, the Mobile API and NCIP all refuse with the same codes:
media_limit_reached,not_loanable,reservations_disabled,reservation_limit_reached(Mobile APICirculationOutcome, NCIP Item Does Not Circulate / Maximum Check Outs Exceeded).GET /api/v1/catalog/books/{id}/availabilitygainsloanableandreservations_enabled. NCIP RequestItem due dates are now capped by the maximum loan duration like every other channel (they used to take the bare loan duration). - Reservation limits per patron type. A type without reservations does not see "reserve when available" on the book page and a request that would join the waitlist is refused (
reservations_disabled);max_prenotazionicounts the active queue reservations plus the loan requests still awaiting approval (reservation_limit_reached). Website, app, NCIP RequestItem and the admin reservation form apply them; the admin form can override them with an explicit "Ignora i limiti di prenotazione" tick (override_limits: truefrom the staff app). - Card duration of the patron type. When the type sets a card duration, the card expiry is computed on creation and edit ("Calcola la scadenza dalla durata tessera", on by default when creating, where it fills an empty date only: a date typed by the librarian wins), on self-registration and on registration approval; the date can still be set by hand. The reader page has a "Rinnova tessera" button: expiry = today + the type's duration (one year when the type sets none), an expired account becomes active again, and the renewal is in the activity log.
- "Tessera in scadenza" email (
card_expiring, five languages). Sentloans.card_expiry_warning_daysbefore the card expires (Settings → Loans, default 0 = off) bycron/automatic-notifications.phpandcron/full-maintenance.php(both report the count), once per expiry date (utenti.card_expiry_warning_sent_for); a renewed card re-arms it. Fresh installations get the template seeded in the fivedata_*.sqlfiles (row 29) like every other template; upgrades use the shipped text until the template is saved. - Administrators only. Patron types and the rules matrix (Settings → Circulation, the type pages and every form behind them) are reserved to administrators and to custom roles with the settings permission; a staff account is sent back to the dashboard and nothing changes. For staff the Circulation section is absent from the settings navigation and
/admin/settings?tab=circulationopens the default section without the circulation panel.
Closures calendar
- The library's closed days. A new admin page, Admin → Calendario chiusure (
/admin/calendar, also linked from Settings → Loans), holds the weekly closed days (loans.closed_weekdays) and the closures: single days or periods, once or every year, for every branch or for one. Public holidays come from a country preset (see below), any other calendar can be imported from an ICS file, and a monthly preview shows which days are open. - Due dates land on an open day. The shift lives in
CirculationPolicy::dueDateFor(), so the per-patron-type durations and renewal days of the circulation policy land on an open day too. A due date that falls on a closed day moves to the next open day: admin loans (proposed and saved), loan edits, reader requests from the website and the app, reservations and their promotion to a loan, renewals and bulk extensions, NCIP CheckOut, RequestItem and RenewItem. A closure of one branch counts for the loans of its copies: when the staff desk lends a copy, an NCIP CheckOutItem binds one, or a request is approved onto one, the due date moves to that branch's next open day (at approval only if the longer window still fits; the NCIP checkout is refused, like the desk's, when the longer window no longer fits). A refused renewal proposes the last open day before the conflict. - Pickup days are open days. The pickup deadline counts only the days the library is open, and the nightly sweep expires no pickup, no scheduled loan and no queue reservation on a closed day; a deadline that falls on a day closed afterwards moves to the next open day when the calendar is saved, so the desk can still hand the book over that day, but never past the loan's due date (a pickup after the loan window is refused), which the calendar save does not move.
- The request calendars know the closed days. On the book page and in the admin loan form a closed day cannot be picked as the start (a request that reaches the server with one starts on the next open day); the availability payload carries
closed_dates, on the website and in the Mobile APIGET /catalog/books/{id}/availability(additive field). A request auto-approved on a closed day is reported byPOST /reservationswith its real state,prenotato(new enum value) with the start day instart_date(additive), instead ofda_ritirare. - ICS imports meet the form's limits. Every event is checked like a closure typed in the form (a yearly closure within a year, a one-off within ten years, end not before start); the import message counts the events discarded. A yearly closure of a year or more, however stored, is applied once as the interval it names and never closes every day; a one-off closure of any admissible length (up to ten years) is stepped over whole, so a date inside it lands on the day after it ends; an ICS date-time in UTC is read in the application timezone. The calendar page follows the settings permission (
settings.manage), so a custom role with that permission can use it. - With no closed weekday and no closure configured every date is computed exactly as before.
Public holidays for every country, and the library's own holidays
- Pick the country, then the holidays. The holiday card of
/admin/calendarreplaces the Italian-only import: the librarian chooses the country (Italy, United States, United Kingdom, Ireland, Germany, France, Denmark, Spain, Portugal, Netherlands, Belgium, Austria, Switzerland, Sweden, Norway, Finland, Poland, Canada, Australia, Brazil, Argentina, Mexico; preselected from the installation language) and, where holidays differ, the region (German Länder, UK nations, Canadian provinces and territories, Australian states and territories, Alsace-Moselle, six Swiss cantons), sees every holiday with its native name and its date in the preview year, ticks the ones the library closes for (select all / none; customary holidays such as Christmas Eve start unticked) and applies. - Rules, not dates. Fixed dates, Easter offsets (Good Friday, Ascension, Whit Monday, Corpus Christi, Carnival…), the nth weekday of a month (Thanksgiving, Memorial Day, bank holidays), the weekday on or before/after a date (Victoria Day, Buß- und Bettag, Midsommar), the weekend substitutes of the US, UK, Ireland, Canada and Australia (including Christmas and Boxing Day both on a weekend), Koningsdag, the Argentine movable holidays, first and last years (Store bededag until 2023, Juneteenth from 2021, San Francesco in Italy from 2026, Wigilia in Poland from 2025) and the one-year moves of the early May and spring bank holidays (VE Day 2020, the Jubilees).
- Applying is idempotent and selective. The choice is stored in
calendar.holiday_country,calendar.holiday_regionandcalendar.holiday_rules; each ticked holiday becomes closures marked with its rule (calendario_chiusure.regola_festivita): a fixed date as one yearly closure, any other holiday dated per year up to two years ahead, extended every year by the maintenance run (holiday_rows_addedin the cron log). Applying again adds nothing, unticking removes only that holiday, switching country replaces the preset; closures added by hand or imported from ICS are never touched. Holiday rows show a "Festività" badge in the list and are changed from the selection only. Rows stored by the former Italian import are recognised and adopted, not duplicated. - Holiday rules also cover distant and historical dates. The dated rows are the admin preview, not a limit on loan policy: due dates, pickup days, renewal caps, availability and late-fine days evaluate the selected rules for the year requested, including an observed New Year that falls in December of the preceding year. Switching the calendar back on works immediately, without waiting for maintenance. Applying a selection and yearly upkeep share a transaction lock and current reads, so a concurrent save cannot restore an unticked holiday or duplicate its rows; manual and ICS closures remain independent.
- Yearly closures on and off. The yearly closures added by hand or from ICS are listed in their own card and can be suspended and resumed without deleting them (
calendario_chiusure.attiva); a suspended closure stays in the list with a "Sospesa" badge and closes no day. - Every country of the world. Besides the 22 hand-written rule tables, the country list holds every country and territory (250) with its official holidays from the open data of the python-holidays project (MIT licence,
app/Support/Holidays/data/, years 2010-2060, regenerated byscripts/generate-holiday-dataset.py): Islamic, Chinese, Hindu, Buddhist, Orthodox and national holidays alike, in the country's own language with the English name beside it. Holidays of public offices, banks, schools, half days and those of a single community (Islamic, Hindu, Jewish, Orthodox, Yazidi…) are offered unticked with their category; dates that depend on the sighting of the moon are marked as estimated. Country names follow the interface language. - A chosen holiday that vanished is said, not silently dropped. When a stored holiday id is no longer in the rule table or the dataset (a regeneration accepted with
--accept-removed), the holiday card shows which chosen holidays close nothing any more, so the librarian can tick their replacements and save again;HolidayPresets::selection()lists them asmissing. - Days in lieu close both days. When a weekend holiday gets a substitute (US, UK, Ireland, Canada, Australia), the library is closed on the holiday itself and on the substitute; holidays that are moved instead (Koningsdag, the Argentine movable holidays) keep only the new date. Easter Sunday in Western Australia (from 2022), the Northern Territory (2022) and South Australia (2024), and the extra Monday of a weekend Anzac Day in Western Australia.
- The library's own holidays. A new card "Festività della biblioteca" (table
festivita_personalizzate) adds holidays the official lists do not have or that the community celebrates: the same day every year, the nth weekday of a month, days from Western or Orthodox Easter, a date in the Islamic (Umm al-Qura), Hebrew, Chinese (published lunar table, 1900-2100), Persian, Indian national, Ethiopian or Coptic calendar, or dates listed by hand; one or more days, a weekend rule, first and last year, every branch or one. They apply to every year, can be suspended, edited and deleted, and show their next dates. An installation that defines none behaves as before.
Fines, patron ledger and debt block (plan §7)
- Late fines. Settings → Loans → "Multe e blocco per debito": daily fine, grace days, maximum fine per loan. These are the global layer of
CirculationPolicy: a patron type (Settings → Circulation) can set its own daily fine, grace days, cap per loan and debt threshold, and a cell of the rules matrix its own fine, cap and grace for one material; an empty field inherits, 0 switches fines (or the block) off for that type or material. Late days are the days the library is open (closures calendar: weekly closed days, closures, yearly closures, branch closures for the copy's branch); with no closure configured every day counts. The grace period is a threshold (SLiMS rule): up to the grace days nothing is charged, beyond it every late day counts from the first. With the daily fine empty or 0 (the default) nothing changes. The three amounts accept up to 9999.99 (the form says so when a larger figure is typed), and the fine of one loan never exceeds what the ledger stores (99,999,999.99), however long the loan was overdue. The fine is written at return, in the same transaction as the close, on every return path (return screen, quick return, NCIP check-in), and only once per return (idempotency keylate:<loan>). Fines accruing on open overdue loans are shown on the loan, the reader record, the reader's own page and the app, never written in advance. - Patron ledger. New append-only table
movimenti_lettore(charges, payments, waivers, reversals); the balance is the sum of the rows. The reader record has a "Movimenti" section: balance, accruing fines, list, register a payment (cash, card, transfer, other) with a PDF receipt, manual charge (description required), waiver (reason required). Each desk form is recorded once even when it is sent twice (the same form sent again with another amount, method or text is refused with "Richiesta già registrata" instead of being answered with the earlier movement; a page restored from the browser cache gets a fresh form), and a long ledger is shown 200 rows per page. Recording a payment, a charge or a waiver and downloading a receipt are loan-desk actions: they need the loans permission (loans.manage, as Staff has), so a custom role that only manages readers sees the ledger read-only. The return screen shows the fine before the confirmation and can waive it on the spot, with a reason; a waived fine no longer appears in the return email, the fine email or on the loan page. A reader with ledger rows is suspended instead of deleted. - Debt block. With a threshold set, a reader who owes more than it gets
debt_block. The threshold is the one of the reader's patron type; a reader without a type (or whose type no longer exists) is governed by the default type; a type whose field is empty inherits the globaldebt_block_threshold, and 0 switches the block off for that type. A reader who owes more than the applicable threshold getsdebt_block("Saldo a debito oltre la soglia consentita…") on loan requests, admin loans, approvals, renewals and the reservation queue, on the website, the Mobile API and NCIP. - Reports and email. Statistics: charged, collected, waived and reversed per month with CSV export. Return email: new placeholder
{{multa}}(empty when nothing was charged); an upgraded install whose stored "Restituzione confermata" template is still the previously shipped text gets the line when Settings → Email templates is opened, a customised template is left as it is. New optional email "Multa per ritardo addebitata" (fine_charged), off by default. - Mobile API 1.9.0 (requires 0.9.0):
GET /me/movementswith balance, movements and accruing fines;debt_blockarrives asnot_eligiblewithmeta.reason(and aseligibility_reasonin the staff loan lists). The same plugin version carries the 0.9.0 circulation changes:closed_dates,loanableandreservations_enabledinGET /catalog/books/{id}/availability, the 409 codesmedia_limit_reached,not_loanable,reservations_disabledandreservation_limit_reached, andoverride_limitson the staff reservation form. app.currencyis now read from the database too (it was a code default only).- Fine settings are administrators only. Staff can still save the Loans tab, but the daily fine, grace days, cap per loan, debt threshold and fine email stay as they are unless an administrator changes them; the fields are read-only for staff.
Feature toggles
- Every optional 0.9.0 module has its own switch (
features.*insystem_settings,App\Support\Features). A new Settings → Modules tab, administrators only, switches stocktake, MARC import and export (with copy cataloguing), catalogue cards, suppliers, RDA types, library cards and patron photos, user CSV import and the visitor log; the Loans tab switches the rules by patron type, the closures calendar and fines with the patron ledger, next to their settings. Staff sees the Loans switches locked and does not see the Modules tab; a hand-built post is refused by an inlinemayConfigure()check. - A module switched off is gone: no sidebar or menu entry, no button, no form field, no settings section; its routes answer as catalogue mode does (302 to the dashboard with "Funzione disattivata nelle impostazioni.", or 404
feature_disabledfor JSON callers) throughFeatureGateMiddleware, and the controllers re-check. The services carry the switch for every channel at once: with patron type rules off only the global loan rules apply and consultation-only copies are lendable; with the calendar off due dates ignore closures, the holiday card and the yearly closures card are gone with the calendar page (their posts are refused) and the maintenance run adds no holiday row until it is switched back on; with fines off nothing is charged, nobody is blocked for debt, previews, balances and the fine email are gone andprestiti.sanzioneis written as before; with RDA types off the stored overrides are kept but exports derive the types from the material; with suppliers off a book save or an import never toucheslibri.fornitore_id; with the visitor log off maintenance purges nothing. Each statistics section follows its module. Data is never deleted: switching a module back on shows it again. - Defaults. A fresh install starts with everything on except the visitor log (personal data) and fines (money). An upgrade keeps on what is reachable only from an explicit action (MARC, catalogue cards, library cards, user import, patron type rules, closures calendar) and starts off what adds sidebar entries, cataloguing fields, money or personal data (stocktake, suppliers, RDA types, visitor log, fines), unless the library already has data for the module, which then stays on. A missing row behaves as the upgrade default.
- Catalogue mode still wins: it also switches off the visitor log, the closures calendar, fines and patron type rules, and the Presenze sidebar entry, which used to stay visible in catalogue mode, now disappears.
- Mobile API:
/health.featuresgainsfines,closures_calendarandcirculation_rules; with fines offGET /me/movementsanswers 404feature_disabled.
Fixes
- Installer: no false "triggers missing" warning. Every fresh install ended on a page saying the database user could not create triggers and asking for
installer/database/triggers.sqlto be run by hand, even when every trigger had been created: the importer counted a trigger only when its chunk began withCREATE TRIGGER, while each chunk begins with comments and aDROP TRIGGER. The count now finds the statement wherever it sits, and when a trigger really fails the final page says that some triggers could not be created, instead of blaming the database user's permissions, and shows the database's own message (escaped) under it.
Migration
migrate_0.9.0-rc.1.sql, section "MARC bulk import / copy cataloguing":import_logs.import_typegains the valuemarc, appended to the enum computed from the current one, so the order of the sections does not matter. Idempotent; existing rows are untouched.migrate_0.9.0-rc.1.sql, section "Inventory stocktake": createsinventario_sessioni(withazione_mancanti, the close's choice),inventario_attese(withesito_chiusura, the bucket frozen at close, andcodice_atteso/titolo_atteso, the copy code and title frozen at opening so a copy deleted mid-session stays identifiable in the report; each added with a guard to a table an earlier build created) andinventario_scansioni(UNIQUEsessione_id, copia_id) and seeds the "Revisore inventario" role. Additive and idempotent; nothing existing changes.migrate_0.9.0-rc.1.sql, section "minor features": createspresenze(visitor log) andfornitori(suppliers), addslibri.fornitore_id(ON DELETE SET NULL) and the nullablelibri.rda_content_type,rda_media_type,rda_carrier_typeoverrides. Additive and idempotent.migrate_0.9.0-rc.1.sql, section "patron-cards-import":import_logs.import_typegains the valueusers; the new enum is computed from the current one, somarcand any value added by another section are kept.migrate_0.9.0-rc.1.sql, section "Circulation policy": createstipologie_lettoreandregole_prestito, seeds the default "Standard" type with every rule NULL, addsutenti.tipologia_id(FKON DELETE SET NULL),utenti.card_expiry_warning_sent_forandcopie.solo_consultazione. Idempotent; no existing row changes.migrate_0.9.0-rc.1.sql, section "closures calendar": createscalendario_chiusure(closure intervals, optional yearly recurrence and branch, FK tosediON DELETE CASCADE); the weekly closed days are the settingloans.closed_weekdays, no schema. Additive and idempotent.migrate_0.9.0-rc.1.sql, section "holiday presets": addscalendario_chiusure.regola_festivita(varchar(64) NULL, indexed: the holiday rule a preset row stands for, NULL for manual and ICS rows) andcalendario_chiusure.attiva(default 1). Guarded oninformation_schema, idempotent; existing rows stay manual and active until the first apply adopts the former Italian ones.migrate_0.9.0-rc.1.sql, section "fines-ledger": createsmovimenti_lettoreand converts every existingprestiti.sanzione > 0into one charge (addebito_perditafor a lost copy,addebito_dannootherwise, keypenalty:<loan>), in batches; maintenance converts anything past the batches and records the scan as complete only once the table exists and a pass has run to the end. Until the migration has run, the queue selection and the eligibility check carry no debt predicate.prestiti.sanzionestays and is still written.migrate_0.9.0-rc.1.sql, section "feature toggles": onefeatures.<key>row per module with the upgrade default, insert-if-missing (a saved value is never overwritten); stocktake, suppliers, visitor log, RDA types and fines start on when the library already has sessions, suppliers, visits, RDA overrides on live titles, ledger rows other than the converted legacy penalties, or a configured daily fine. The fresh-install defaults are in the fivedata_*.sqlseeds.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.