Skip to content

Staging - #375

Open
MostafaKadry wants to merge 123 commits into
stagingfrom
develop
Open

MostafaKadry wants to merge 123 commits into
stagingfrom
develop

Conversation

@MostafaKadry

Copy link
Copy Markdown
Collaborator

No description provided.

NotAbdelrahmanelsayed and others added 30 commits June 9, 2026 20:36
Adds a "LIFO Cart Order (Newest on Top)" POS setting. When enabled and
no explicit cart sort is active, the most recently added item is shown at
the top of the cart instead of the bottom — handy on long carts where the
cashier wants to see what was just scanned.

- New cart_lifo check field on POS Settings (default off) + backend
  constants (POS_SETTINGS_FIELDS / DEFAULT_POS_SETTINGS).
- posSettings store exposes a cartLifo computed.
- useCartSort accepts an optional lifoMode; reverses the list when LIFO is
  on and no sort column is selected. Explicit sorts are unaffected.
- InvoiceCart passes the setting through; toggle lives in the Sales
  Operations settings group so it takes effect live via reloadSettings().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Credit (Pay-on-Account) sales inflated the POS shift closing total.
`_process_invoice()` added the full `base_grand_total` of every invoice to
the sales summary (`grand_total`/`net_total`/`sales_total`) and the per-row
amount, regardless of how much was actually paid. The cash Payment
Reconciliation, however, is built only from real payment rows — so a pure
credit sale pushed Net Sales up by the full amount while contributing 0 to
the drawer, leaving the two figures inconsistent within a single shift.

For non-return invoices the money summaries and the per-row `grand_total`
now use the amount actually collected (`base_paid_amount`): a pure credit
sale contributes 0, a partial sale contributes only its down-payment, and
`net_total` is scaled by the paid ratio. The full invoice value is preserved
in `transaction_amount`; display-only `invoice_total` and
`outstanding_amount` are added for the dialog badge and stripped before the
child-table set. Returns, quantities and tax accrual are unchanged.

The Close Shift dialog now shows an "On Account" / "Partially Paid" badge and
an "Unpaid: {amount}" sub-line on rows collected for less than their invoice
value. Adds unit tests for the collected-money totals and an Arabic string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…closing-total

# Conflicts:
#	POS/src/components/ShiftClosingDialog.vue
#	pos_next/pos_next/doctype/pos_closing_shift/pos_closing_shift.py
#	pos_next/pos_next/doctype/pos_closing_shift/test_pos_closing_shift.py
#	pos_next/translations/ar.csv
…rder

# Conflicts:
#	POS/src/components/sale/InvoiceCart.vue
#	POS/src/components/settings/POSSettings.vue
#	POS/src/composables/useCartSort.js
#	POS/src/stores/posEvents.js
#	POS/src/stores/posSettings.js
…evelop.

Keep origin/develop baseline offers; GWP/auth Python stay in posnext_promotions. Free-bundle SI merge is no longer in the POS Next invoice class.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Updated App.vue to conditionally render AuthorizationDialog based on authGateInstalled.
- Modified SelectInput.vue to support multiple selections and improved UI for selected options.
- Refactored CreateCustomerDialog.vue to use requiresSplitCustomerName for customer name handling.
- Adjusted InvoiceCart.vue to utilize the new wallet API and removed Magento dependencies.
- Enhanced PromotionManagement.vue to include Gift Pool promotion type and related UI elements.
- Introduced applyOfflineGiftPool function in posCart.js for handling gift pool discounts.
- Updated various API endpoints to ensure compatibility with new promotional features.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Updated useInvoice.js to prevent free items from inflating stock checks and adjusted actual_qty to prefer original_stock.
- Improved posCart.js to utilize new stock validation functions for accurate quantity checks, ensuring free items do not affect paid item stock validation.
- Added utility functions in stockValidator.js for calculating stock quantities and validating item availability based on combined paid and free rows.
- Modified _collect_stock_errors in invoices.py to sum quantities per item_code and warehouse, ensuring accurate stock error reporting.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Added conditional rendering for buttons in InvoiceCart.vue to prevent actions on locked free rows.
- Updated quantity handling logic to ensure free items do not interfere with paid item interactions.
- Introduced isLockedFreeRow function to streamline checks for free item status.
- Enhanced posCart.js to manage free item quantities more effectively, ensuring accurate invoice processing for promotions like GWP.

Co-authored-by: Cursor <cursoragent@cursor.com>
Addresses review from @MostafaKadry on PR #312:

- paid_amount is the raw tendered total, so cash change given back to
  the customer (e.g. $20 tendered on a $15.50 sale) was inflating
  "collected" and driving outstanding_amount negative. Netted
  base_change out of base_paid before it's used anywhere, and reused
  the value for the existing cash-reconciliation subtraction instead
  of computing it twice.
- net_total/grand_total scaled by paid_ratio for partial/credit sales,
  but the tax loop still aggregated the full tax_amount. Scaled taxes
  by the same ratio (1 for returns) so grand_total stays consistent
  with net_total + taxes.
- Reformatted test_pos_closing_shift.py to tabs (repo convention) and
  added regression tests for both fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TngwKeKiHtHt6jFP9pcnY6
…them

Reworks the credit-sale closing fix so the cash-basis numbers live in their
own fields rather than overwriting the accrual ones.

grand_total, net_total and the taxes table go back to the invoiced amount.
Two new read-only fields on POS Closing Shift carry the cash view:

  collected_amount   money actually taken during the shift
  outstanding_total  invoiced value still owed by customers

invoiced == collected + outstanding, asserted in the tests.

Why the change of approach:

- POS Closing Shift.grand_total is persisted and already read elsewhere —
  get_shift_history() surfaces it as the "Sales" column and summary card.
  Redefining it as "collected" would have left historical rows meaning
  "invoiced" and new rows meaning "collected", with nothing to tell them
  apart. It now keeps its meaning and Shift History needs no change.

- Scaling taxes by the paid ratio kept grand_total = net_total + taxes, but
  tax posts to the GL in full at invoice submission regardless of what was
  collected, so the scaled table could not be reconciled against the VAT
  accounts. With nothing scaled the identity holds by construction.

- The ratio also misreported write-offs: a 100 invoice settled by 90 cash
  plus a 10 write-off reported net_total 90 and tax 0.9. Covered by a
  regression test.

Also:

- Port the same cash-basis logic to pos_closing_shift.js. The desk form
  mirrors _process_invoice(); leaving it on the old path would have made
  closing a shift from the desk and from the POS produce different totals
  for the same invoices.

- Closing dialog gains "Collected" and "On Account" cards, so the cashier
  can still see what was sold, not only what was banked. Per-invoice badges
  now key off collected_amount, since grand_total is invoiced again.

- Arabic strings for the three new labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
- Updated CreateCustomerDialog.vue to require a selected country code for customer submission and improved mobile number handling.
- Enhanced posCart.js and posOffers.js to include gross and net amount calculations for offer validation, ensuring accurate eligibility checks based on item discounts.
- Refactored customer group and territory resolution logic in customers.py to prefer non-group entries, improving customer creation reliability.
Dead assignment — the value is never read. It predates this branch, but
ruff lints changed files, so it fails pre-commit for anything touching
this module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
Merges upstream/develop (42 commits) into the product management branch.

Four files conflicted. All were resolved by taking develop's version and
re-applying only the functional additions, so none of this branch's
prettier churn is reintroduced:

- itemSearch.js — reset to develop, then re-added upsertItemInList(),
  refreshItem() and the store export. Verified by normalizing both files
  through the same formatter: the delta against develop is now exactly
  those two functions, the export, and one let -> const. Previously the
  branch showed ~600 changed lines here, almost all quote style and
  trailing commas.
- ManagementSlider.vue — took develop's formatting, re-applied the
  Products -> Stock Lookup rename and the new Product Management button.
- POSSale.vue — took develop's `let` declarations. The biome-ignore
  comments are unnecessary; JS is linted by prettier/eslint in
  pre-commit, not biome.
- ar.csv — kept both sides' new strings.

No functional change to the feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
The item-group scoping added in review covered get_products (the read
path). The same boundary was not enforced when writing, in two ways.

1. pos_profile was caller-supplied and never validated.

   save_product() took the profile name straight from the request and
   called frappe.get_cached_doc(), which does no permission check. The
   guard that follows is:

       if allowed_item_groups and item_group not in allowed_item_groups:

   and _get_pos_profile_allowed_item_groups() documents that an empty
   list means "no group restriction". So naming any POS Profile with no
   item_groups rows made allowed_item_groups falsy and skipped the check
   entirely — a cashier scoped to one branch could edit any Item in the
   system. It also reached pricing, since the price is written to that
   profile's selling_price_list.

   Added _validate_pos_profile_access(), matching the POS Profile User
   check already used in api/invoices.py and api/credit_sales.py, and
   applied it to save_product, get_products and get_item_groups.

2. The update path never checked the item's current group.

   Only the incoming item_group was validated, so passing the item_code
   of an item in a disallowed group pulled it into an allowed one —
   renaming, regrouping, disabling and repricing it on the way. Now the
   existing group is validated before the item is mutated.

Neither is exploitable without Item write permission, which the feature
requires anyway. But the point of profile scoping is to hand product
management to a branch cashier without giving them the global item
master, and that boundary did not hold.

Also in this file:

- get_item_groups() had no permission check at all — any authenticated
  user could enumerate any profile's item groups. Now requires Item read
  plus profile access.
- item.image accepted an arbitrary string, so a crafted call could point
  it at an external URL that the POS then renders. Restricted to Frappe
  file paths. (Reachable from the desk too, so not introduced here.)
- Removed frappe.db.commit(). The framework commits at the end of a
  successful request and rolls back on exception; committing here
  defeats that and commits unrelated pending work.
- Sorted the import block (ruff I001), which otherwise fails pre-commit
  for anyone touching this file.

Adds test_product_management.py — 10 mock-based tests, no DB. Verified
by mutation: removing the profile check fails
test_rejects_profile_the_user_is_not_assigned_to, and removing the
current-group check fails test_rejects_item_whose_current_group_is_out_of_scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
Two formatting-only changes, no behaviour change:

- ProductManagement.vue — ran the repo's prettier over the new file.
  It is a new file, so this is not churn against develop; pre-commit
  would rewrite it on merge anyway.
- itemSearch.js — braced the four switch-case blocks that declare
  consts (eslint no-case-declarations, 8 errors). This is pre-existing
  on develop and only surfaces here because pre-commit lints changed
  files and this branch touches the file. Braces just scope the consts
  to their case.

Verified afterwards that itemSearch.js and POSSale.vue still differ from
develop only by this branch's functional additions — no formatting churn
was reintroduced into either.

Split out so it can be dropped independently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
Two defects stacked, so a failed save showed nothing at all.

1. handleError was undefined.

   ProductManagement.vue destructures it from useToast():

       const { showSuccess, showError, handleError } = useToast();

   useToast() never exported handleError. All four catch blocks —
   saveProduct, loadProducts, loadItemGroups, loadUOMs — therefore called
   undefined(...), throwing a TypeError *inside the catch*. That
   rejection is unhandled, so nothing reaches the user: the spinner stops
   in `finally` and no toast appears.

   The file was adapted from PromotionManagement.vue, which defines
   handleError locally (line 1449) instead of taking it from useToast.
   CouponManagement.vue has a third copy. Same class as the refreshItem()
   issue already caught in review.

2. Reading _server_messages does not work for errors raised through the
   app's `call` wrapper.

   frappe-ui's call.js does not pass the response through. It builds a new
   Error whose `.message` is only "<method> <exc_type>", and moves the
   real text to `.messages` — already parsed out of _server_messages and
   flattened to strings. So `error._server_messages` is undefined and the
   user gets:

       pos_next.api.product_management.save_product ValidationError

   The local copies in PromotionManagement.vue and CouponManagement.vue
   check only _server_messages, so they have this same blind spot.

Fixed at the root: parseErrorMessage + handleError now live in useToast
and are exported, so the existing call sites become correct unchanged.
parseErrorMessage handles both shapes — frappe-ui's `.messages` array
first, then raw `_server_messages` for the direct fetch() paths — strips
the HTML, de-duplicates, and ignores a `.message` that is just the
"<method> <exc_type>" string. handleError never throws, so a failing
error handler cannot turn a failed action into a silent one again.

Promotion/Coupon keep their local copies for now; they work for raw
responses and migrating them is out of scope here.

Verified against a real failure — an Item save rejected by the Shopify
on_update hook in ecommerce_integrations. Reconstructing the error object
exactly as frappe-ui/src/utils/call.js builds it, the toast goes from
"pos_next.api.product_management.save_product ValidationError" to
"Failed to decrypt key Shopify Account… Encryption key is invalid!
Please check site_config.json…". Null, plain Errors, empty message
arrays and exc_type-only errors all fall back to the default message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
…ct image

The form was capped at `max-w-2xl` (672px) inside a pane that is ~1500px
wide on a 1080p screen, so roughly 830px sat empty while the product
image was a 128x128 thumbnail.

Layout

- Dropped the max-w-2xl cap; the form now uses the pane up to 1400px.
- Two columns from xl: fields on the start side, a large image panel on
  the end side. CSS grid flips this for RTL on its own, so the image
  stays on the end side in Arabic without extra rules.
- The image panel is sticky on xl+, so it stays visible while scrolling
  the UOM conversions.
- Below xl it collapses to one column with the image on top, capped at
  320px so a square image does not fill a tablet screen.

Image

- 128x128 thumbnail -> a square panel that is 380px at xl and 460px at
  1536px+. About 3x the linear size, 8-13x the area.
- The image itself is the upload target: click anywhere on it, or drop a
  file onto it. Drag state is reflected on the border.
- object-contain rather than object-cover, so product photos are not
  cropped.
- Empty state is now an explicit dashed dropzone instead of a grey box.

Fields

- Grouped into Details / UOM Conversions / Options sections with
  headings, rather than one flat stack.
- Product Name spans the full width; Item Group, UOM and Price share a
  row once there is room for it.
- The two checkboxes became labelled cards with descriptions.
- UOM conversion rows use an icon-only remove button, which stops the
  row wrapping in the narrower column.

Breakpoints

Tailwind breakpoints are viewport-based, not container-based, so a naive
`sm:grid-cols-2` inside the fields column fires while that column is
still ~190px. Column widths were worked out per viewport and the
breakpoints chosen to match: two columns only from xl (fields 436px),
inner grids only from 1536px (fields 599px).

Note: `min-[1536px]:` is used rather than `2xl:` because frappe-ui's
preset replaces theme.screens with sm/md/lg/xl only
(frappe-ui/tailwind/plugin.js:212). `2xl:` compiles to nothing in this
project — verified by checking the emitted CSS.

Verified every new utility and arbitrary value is present in the built
stylesheet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
…Settings

Mobile

The app is responsive (device-width viewport, useResponsivePayment's
isMobileView, mobile detection in performanceConfig, PWA), but this
dialog was not: a fixed 320px list beside the form does not fit a phone.

- Below md the list and the form are one pane at a time (master-detail),
  with an explicit back arrow in the form header since the list is no
  longer beside it. The arrow mirrors under rtl:.
- Full-bleed on phones; the 95vw/95vh rounded card returns at sm.
- Header, action bar and form padding tighten below sm; the action bar
  wraps rather than overflowing, and the product title truncates.

Upload rules

Extensions and max size were hardcoded (2 MB, four MIME types), so the
picker accepted files the server then rejected. Both now come from
System Settings via get_product_image_settings():

- allowed_file_extensions is newline-separated, uppercase, without dots,
  and empty means "no restriction" rather than "nothing allowed". It
  covers every file type, so it is intersected with the image types this
  screen can render — a site allowing CSV should not offer CSV as a
  product image.
- max_file_size is stored in MB; get_max_file_size() resolves it to
  bytes, falling back to site_config then Frappe's 25 MB default.

Validation matches on extension rather than MIME type, because browsers
report inconsistent types for the same file and the server checks the
extension too. If the fetch fails the screen keeps working on defaults;
the server validates on upload regardless.

The hint under the image now reads the live values:

    Allowed file types: JPG, JPEG, PNG, GIF, WEBP — up to 25 MB.
    These are configurable in System Settings. Upload happens when the
    product is saved.

If the site allows no image types at all, that is stated explicitly
instead of leaving a picker that can never succeed.

Adds 5 tests (15 total) covering the empty-means-unrestricted rule, the
intersection with image types, the no-image-types case, messy operator
input (lowercase, leading dots, blank lines) and the MB-to-bytes
conversion. Verified against the live site: no restriction configured,
so all five types at 25 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMeLBCSBBYMm5k5zDNda4
MohamedAliSmk and others added 29 commits September 14, 2026 14:15
- Updated EditItemDialog.vue to prevent price-list refresh for locked free items and scale rates when changing UOM for promotional items.
- Modified InvoiceCart.vue to ensure free promotional rows remain promotion-owned, preventing unintended edits.
- Enhanced posCart.js to apply UOM changes while preserving rates for free promotional items, ensuring accurate pricing and user feedback.

This update aims to improve the handling of UOM changes and pricing integrity for promotional items in the sales process.
…ettings

- Added `merge_pos_settings` function to ensure runtime integration flags (`miraaya_installed`, `magento_loyalty_available`) are always present in POS settings, even when not defined in the database.
- Updated various modules to utilize the new function, ensuring that default settings are correctly merged with database values.
- Enhanced test cases to validate the behavior of merged settings, maintaining expected defaults and integration flags.

This update aims to improve the reliability of POS settings by ensuring critical flags are always available, enhancing integration capabilities.
… feature/thin-develop-split

Resolve CreateCustomerDialog JSDoc conflict; keep detailed ISD defaulting comment.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ofiles by creation date

feat(posCart): normalize pricing rules to prevent corruption from server responses
feat(authorization): prevent multiple gated requests from overwriting state
frappe-ui selects portal at z-100 behind the z-300 management dialog, so the option list never appears. Use SelectInput and raise portaled select menus so cashiers can filter and edit coupons.

Co-authored-by: Cursor <cursoragent@cursor.com>
…n-list

fix(promotions): show coupon filter dropdowns above the overlay
…not-reflected-in-pos-screen

Pn 111 item code settings not reflected in pos screen
…ify app behaviors

- Added a section on bootstrapping dependencies for the `pos_next` app, detailing commands to ensure required and optional apps are installed on a site.
- Included a table summarizing the behavior of `erpnext`, `posnext_promotions`, and `pos_next` during the bootstrap process.
- Removed redundant module entries from `modules.txt` for clarity.
pos.html referenced /assets/nexus_demo/...bundle.js directly. That URL never
changes and Frappe serves it with a 12-hour max-age, so any browser that had
already opened POS kept running the previous build for half a day — including
straight through the release that added the role switcher to the banner. The
Desk picked the change up immediately, which made it look like a POS bug rather
than a caching one.

bundled_asset() resolves the same file through assets.json, so the URL carries
the build hash and a new build is a new URL. include_style() also picks the
mirrored css-rtl build for an Arabic session, which the hardcoded path never did.

The tags moved to the end of <body>: vite parses <head>, and loose text there is
liable to be relocated into the body, which would separate these from the
conditional guarding them. That conditional keeps the tags off sites where
nexus_demo isn't installed, which is what the old onerror="this.remove()" was
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9wyGLvRjk5q4rjDRrmn6t
Load the demo banner by its hashed URL, not a fixed path
F4/F8/F9 only skipped when uiStore.isAnyDialogOpen was true, but the
session-lock screen and clear-cache overlay aren't registered in that
dialog tracker, so the shortcuts still fired underneath them. Also fix
F8 being a no-op when a customer is already assigned to the cart, since
the search input isn't rendered in that state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POSSale claimed the whole viewport with an inline height: 100vh; max-height:
100vh. Nothing above POS was accounted for, so when a host put chrome at the top
— Frappe's navbar, the Nexus demo countdown banner, anything that pads the body —
the page did not shrink, it moved. Its bottom ran off the screen by exactly the
height of that chrome, taking the cart's Checkout and Hold buttons with it.

On a desktop that is 48px and the buttons land underneath POSFooter, which is
position: fixed and therefore does not move with the rest of the page: they render,
they look enabled, and they swallow the click. On a phone the demo banner wraps to
two rows and it is 86px, which is more than the footer is tall, so the buttons go
off the bottom edge entirely. Either way the visitor cannot take a payment, which
on a point of sale is the whole job.

The page now sizes itself with flex — html/body/#app form a column and POSSale
fills what is left — so it is whatever the host actually leaves it, with no number
to keep in sync. 48px of banner, 86px of a two-row one, or nothing at all all work
out on their own.

The footer keeps position: fixed, and the integrity checks that enforce it are
untouched. It publishes its measured height as --pos-footer-h instead, and the page
reserves that band with padding-bottom, so the strip stays pinned and visible
without sitting on top of the buttons. The footer owns the number, so restyling it
moves the reservation with it rather than stranding a constant elsewhere.

Verified in a browser at 1280x900, 1440x800 and 390x844, in a demo session with
the banner up: Checkout on-screen, clear of the footer, no document overflow, and
a real click through to a submitted invoice (ACC-SINV-2026-00015). jsdom does no
layout, so there is no unit test that could have caught this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ch-CLI-command-that-installs-dependency-apps-for-pos_next-on-a-given-site

docs: update README with bootstrap dependencies instructions and clarify app behaviors
…tcuts-f4-f8-f9

feat: implement F4/F8/F9 keyboard shortcuts
…-to-sales-invoice

[PN 118] feat(invoice): add sales person and coupon details to invoice display and print format
…ewport

fix(pos): the till hung off the bottom of the screen under host chrome
…invoice

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ries it in the GL

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
handleShiftOpened assigned the undeclared _initializedProfile (renamed to
_initializedKey in ecfddc6), so the ReferenceError skipped
startActivityTracking and the lock never fired until a page reload.
Starting tracking also went through the throttled resetTimer, so no idle
timer was armed until the first activity event.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Without it, migrate and install never create the POS Authorization tables,
so submitting any POS return raises TableMissingError.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ource

fix(credit): linked POS return credit booked on the wrong invoice; session lock after shift open

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants