Skip to content

Retail Operations

Who this is for

Sales ops and accounting handling Reyder's retail channel — devices sold through retail partners like AnyMobile rather than through standard wholesale sales orders.

What you'll accomplish: reserve devices for a retail partner, and record the sales as they happen (either one at a time or via a CSV import).


Retail vs. wholesale — which flow to use

Situation Use
B2B customer placing a specific order ("I want these 3 IMEIs") Sales Order
Retail partner holding your stock on consignment and selling whatever moves Retail Reservation + Retail Sale (this doc)

The retail flow is built around the reality that retail partners don't know in advance which specific IMEIs they'll sell. You reserve a batch first, they sell over time, and you record each sale back into the system.

A reservation is required — there's no direct retail-sale path

Retail sale lines have a domain requiring is_retail_reserved=True on the device, with the retail partner matching the sale header's partner. You cannot add a device to a retail sale line unless it was first reserved to that retail partner. If you need to record a one-off sale, create a quick reservation first, then the retail sale.


Part 1 — Retail reservations

A reservation pins a set of devices to a specific retail partner. While active, those devices can't be allocated to other orders.

Step 1 — Open Reserve/Release Devices

Go to Inventory → Device Inventory → Reserve/Release Devices.

Reserve/Release wizard

Pick the selection mode that matches how you want to pull devices:

  • Manual — pick IMEIs individually
  • By manifest — everything on a specific receiving manifest
  • By PO — everything from a specific purchase order
  • By filters — model / storage / grade / color match

Set the retail partner, retail location, and expected return date (when you expect unsold devices back). Click Reserve.

What a reservation does

  • Creates a device.retail.reservation record in Active state.
  • Flags each device with the retail partner and is_retail_reserved = True.
  • Optionally moves the devices to the retail location physically (creates a device.movement record for the audit trail).

You can review active reservations at Inventory → Device Inventory → Retail Reservations.

Retail reservations list

Step 2 (later) — Release devices that didn't sell

When the partner returns unsold devices, open the reservation and click Release Devices. The wizard lets you select the specific IMEIs coming back — those are:

  • Cleared of the retail partner flag.
  • Moved back to your storage location.

Auto-close behavior

You don't need to manually release a reservation once everything's sold. When the last reserved device gets recorded as a retail sale, the reservation auto-closes itself and moves to Released.


Part 2 — Retail sales

Record the actual sales the retail partner made. Each sale closes out specific IMEIs and posts the accounting automatically.

Option A — Enter sales one at a time

Go to Inventory → Device Inventory → Retail Sales → New.

Retail sales list

  1. Set the Retail Partner and Sale Date.
  2. Add one device line per IMEI sold — pick the device, set its Sale Price.
  3. Click Confirm when the sale is complete.

By default, the device picker only offers devices reserved to that retail partner, still Available, QC Complete, and with a positive purchase cost. Sales Managers and Inventory Managers can tick Exception Override and enter a reason to sell a reserved device with a QC/cost exception.

Retail sale form with exception override

That override is intentionally narrow: it does not let you sell an unreserved device, a device reserved to a different partner, a sold/reserved device, a duplicate line, or a line with a zero sale price.

Option B — Import a CSV of sales

Easier when the partner sends you a weekly sales report.

Go to Inventory → Device Inventory → Import Retail Sales.

Retail sale import wizard

Upload a CSV. The wizard recognizes these columns — headers are case-insensitive and matched by substring (IMEI, imei, IMEI Number, etc. all work), extra columns are ignored, blank rows are skipped:

Column Required Notes
IMEI (or any header containing imei) yes Must match a device already in the system
Sale Price (or any header containing price) yes The price this IMEI sold for — must parse as a positive number
Sale Date (or any header containing date) no Per-row sale date; falls back to the wizard's default if absent

You can leave your vendor's extra columns (supplier SKU, export markers, etc.) in the file — they're ignored.

The wizard creates one device.retail.sale record in Draft state and adds a line per IMEI. It does not auto-confirm — you review the draft and click Confirm on the retail sale form to post accounting.

CSV import validates the same retail-reservation and price rules as manual entry. QC/cost readiness is checked when you confirm the draft sale, so managers can review the imported sale and apply an exception override before confirming if needed.

Importing a mix of partners' sales in one file?

Do one file per retail partner. The retail partner is set on the sale header, not per-line — so mixing partners in one file won't work. Each line's device must also be already reserved to that partner.


What happens when you confirm a retail sale

Click Confirm. All in one transaction:

  1. Each device is marked Sold with the recorded sale price and today's date.
  2. The COGS entry posts (DR COGS / CR Valuation) for the sum of the devices' purchase_cost.
  3. A customer invoice is created and posted to the retail partner — one line per device at the sale price, no tax applied.
  4. For consignment devices (owned by Axis), the paired Owner + Consignee Settlement Reports are created and auto-confirmed — same as a standard SO sale. Axis gets their vendor bill.

After confirmation, the retail sale shows its smart buttons for the invoice, the COGS move, and the settlement reports.


Common problems

A device I want to sell isn't on the list

The retail-sale line domain requires is_retail_reserved=True AND retail_partner_id matching the sale's partner. Reserve the device to the partner first (via the Reserve/Release wizard), then return to the retail sale and the device will appear in the picker. If the device is reserved correctly but not QC complete or has zero cost, a manager can use Exception Override.

Confirm button is disabled

Most likely cause: at least one device line is missing a sale price, or the retail partner isn't set on the header.

Confirm says the device is not ready for sale

The device is not QC Complete or has missing/zero purchase cost. Complete QC and fix cost, or have a Sales Manager / Inventory Manager tick Exception Override and enter a reason. The reason is posted to the device and retail-sale chatter for audit.

The CSV import rejects my file

Headers are normalized (lowercased, whitespace → underscore) and matched by substring, so IMEI, imei, IMEI Number, Device IMEI all work — same for Sale Price, price, Unit Price. An optional Sale Date column is also recognized. Blank rows are skipped; extra columns are ignored. What the wizard actually enforces:

  • There must be a column whose name contains imei and one that contains price.
  • Each IMEI must already exist as a stock.lot (the import can't receive new devices).
  • Each IMEI must be retail-reserved to the partner on the sale header — is_retail_reserved=True AND matching retail_partner_id.
  • Sale price must parse as a positive number.

If an individual row errors, the wizard reports the row number so you can fix just that row.

A retail sale was confirmed but no settlement report appeared

Settlement reports only generate for consignment devices (where owner company ≠ selling company). If the devices belonged to Reyder directly, no settlement is needed — the customer invoice alone covers the accounting. Check each device's owner_company_id to confirm.

The invoice was posted to the wrong journal

Retail invoices use the standard sales journal by default. To route them elsewhere, set a specific journal in the retail partner's customer record (Contacts → Partner → Accounting tab). Invoices created going forward will use that journal.


Under the hood

Technical details for developers

Models

  • device.retail.reservation — groups devices flagged for a retail partner
  • retail.reservation.wizard — the reserve/release wizard with selection_mode in manual / by_manifest / by_purchase_order / by_filters
  • device.retail.sale — one sale; contains lines
  • device.retail.sale.line — one per IMEI sold
  • retail.sale.import.wizard — CSV importer

Reservation state machine

draft → active → released → cancelled

  • action_activate() sets state='active', stamps retail_partner_id on each device, optional movement to retail location
  • action_release() clears the partner flag, moves devices back
  • _check_auto_close() runs after each retail sale confirmation; if remaining_count == 0, sets reservation to released

Retail sale action_confirm() sequence

  1. _validate_before_confirm() checks accounting config, sale journal, duplicate lines, availability, retail reservation ownership, positive sale price, and QC/cost readiness.
  2. SELECT ... FOR UPDATE NOWAIT on stock.lot rows to serialize concurrent confirms.
  3. stock.lot.action_mark_sold_retail(sale_price, selling_company) per device — sets device_status='sold', stamps sale_date, retail_sale_id.
  4. consignment.settlement.report.create_paired_reports() for consignment devices (via .sudo()).
  5. _create_retail_invoice() → customer invoice, one line per device, no tax, auto-posted.
  6. _create_retail_cogs_entry() → COGS move, auto-posted.

Selection field gotcha (fixed v2.x)

_create_retail_invoice() previously tried to dict(field.selection) on the storage_capacity field, which failed because Odoo 19 makes related Selection fields callable. The fix checks callable(sel) and invokes it with sel(record) before dict-wrapping. See device_retail_sale.py:244.

Key fields

Model Field Notes
device.retail.sale state draft / confirmed / cancelled
device.retail.sale invoice_id Customer invoice (account.move out_invoice)
device.retail.sale cogs_move_id COGS journal entry
device.retail.sale settlement_report_ids Many2many to settlement reports generated
device.retail.reservation retail_partner_id, retail_location_id, device_count, sold_count, remaining_count