[FIX] stock_ux: import a count onto the quant it belongs to - #1007
[FIX] stock_ux: import a count onto the quant it belongs to#1007mav-adhoc wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Este PR corrige el import de inventario sobre stock.quant para que cada fila aplique el conteo sobre la línea de stock existente (por clave natural: producto, ubicación, lote, paquete, propietario), evitando duplicar quants y que el conteo se “sume” al on-hand. Además, mejora el feedback cuando se intenta importar columnas no permitidas en un ajuste de inventario.
Changes:
- Override de
_load_records_createenstock.quantpara resolver (o crear) el quant objetivo y escribir el conteo sobre él. - Normalización del lote importado (homónimos entre productos) antes del match por clave natural.
- Nuevos tests que cubren reemplazo vs suma, conteo 0, creación cuando no existe, match correcto de lote y error con columna no importable.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| stock_ux/models/stock_quant.py | Implementa el match por clave natural en import y escribe el conteo sobre el quant existente; agrega validación de columnas importables con error más explícito. |
| stock_ux/tests/test_quant_import.py | Agrega suite de tests para asegurar que el import reemplaza cantidades (incluido 0) y respeta lote/producto. |
| stock_ux/tests/init.py | Incluye el nuevo archivo de tests en el paquete. |
| stock_ux/README.rst | Documenta el nuevo comportamiento del import de conteos y el error más explícito. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def _write_imported_quantity(self, quant, vals): | ||
| """Apply an imported row on the quant it matched, the same way core applies it on | ||
| the quant it gathers when it is not importing.""" | ||
| quant = quant.sudo().with_context(inventory_mode=True) |
There was a problem hiding this comment.
Right, dropped. The lookup now runs with the author's own rights and the write in their own environment with inventory_mode=True, which is the path the editable list itself takes (this module grants write on stock.quant to the adjustment group). The whole matching branch is behind that group now: without it the rows fall back to what core answers. test_import_without_the_adjustment_group_changes_nothing covers it and fails on the previous version of this branch.
| for index, vals in enumerate(values): | ||
| self._normalize_import_lot(vals) | ||
| quant = self._find_quant_by_natural_key(vals) | ||
| if quant: | ||
| records[index] = self._write_imported_quantity(quant, vals) | ||
| else: | ||
| to_create.append((index, vals)) |
There was a problem hiding this comment.
Confirmed and fixed: only a row carrying inventory_quantity or inventory_quantity_auto_apply is treated as a count, anything else is left to core. test_row_without_a_count_leaves_the_line_alone reproduces the old behaviour, where the existing line came out counted as 0.
| def _set_import_default_location(self, values): | ||
| """Same default location core applies while importing, needed before the lookup so | ||
| matching and creation agree on where the row lands.""" | ||
| warehouse = self.env["stock.warehouse"].search([("company_id", "=", self.env.company.id)], limit=1) | ||
| for vals in values: | ||
| if "location_id" not in vals: | ||
| vals["location_id"] = warehouse.lot_stock_id.id |
There was a problem hiding this comment.
Not a crash: search(..., limit=1) returns an empty recordset and warehouse.lot_stock_id.id is False on it, which is exactly what core writes into the row in its own _load_records_create. The row then fails on the required location with the message core gives. Kept as is so that matching and creation agree on where a row lands.
68f3a81 to
22ec131
Compare
22ec131 to
0c17e05
Compare
While importing, core skips the lookup of the existing quant to keep one row = one record, so every row created a second line for a product that already had stock in that location: the counted quantity ended up added to the stock on hand instead of replacing it, and a counted 0 did nothing at all. Resolve the quant by its natural key (product, location, lot, package and owner) and write the count on it, creating the line only when there is none. The lookup returns one record per row, in order, so the invariant core protects is preserved. A lot name is only unique per product, so re-point a lot the importer resolved to another product's before matching. Only a row that actually counts is a count, and only someone allowed to adjust inventory gets one applied. A row with no counted quantity column, or with that cell left empty, names its line without saying anything about it: counting part of an exported file is the ordinary file, and the rows left blank must come out untouched rather than emptied. A user without the adjustment group falls back to what core answers rather than writing as superuser.
0c17e05 to
36acf8c
Compare

What
While importing,
stock.quant.createskips the lookup of the existing quant so that one row stays one record. Every imported row therefore created a second line for a product that already had stock in that location, and the counted quantity ended up added to the stock on hand instead of replacing it. A counted0did nothing at all, because the count landed on a brand new empty line._load_records_createnow resolves the quant by its natural key — product, location, lot, package and owner — and writes the count on it, creating the line only when there is none. It is the inverse of the skip core does while importing.Notes
_load_recordszips its result with the rows it was handed). Rows sharing a key are the same line: the last count wins, instead of two lines that core's merge would later add up.name_search, so a lot that resolved to another product's is re-pointed before matching. Without that, a tracked product duplicates its line all the same.inventory_quantity_setis written explicitly: writing a counted0over a line that was never counted is a no-op write, the compute would not run, and the line would show no difference and stay out of "Apply all".0— the converter turns both into the same0.0, which is why they are told apart while the rows are still the strings of the file.stock_ux.group_stock_inventory_adjustmentthe rows fall back to what core answers, instead of being written as superuser. The lookup runs with the author's own rights, so a row can only land on a line they are allowed to see.Test plan
stock_ux/tests/test_quant_import.py, 9 tests: an existing line is replaced and not added up; a counted0empties the line; a line is still created when there is none; the lot matched is the one of the row's product; the inventoried quantity column applies on the right line; two rows for the same line count it once; a row with no counted quantity column leaves the line alone; a row with that cell empty leaves the line alone; an import by a user without the adjustment group changes nothing.Ran on 19.0:
0 failed, 0 error(s) of 17 tests. With the model file reverted, 6 of the 9 fail — e.g.AssertionError: 2 != 1 : the count must land on the existing line. The other three are guards, each failing against an earlier version of this branch:an empty cell is not a counted 0,no row said 0, and the count being applied without the group.Task: https://www.adhoc.inc/odoo/all-tasks/12857/all-tasks/71351