Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions lims_stock/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
==========
LIMS Stock
==========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:24c1e3996eb14ed2424b5b732c785d352a67ab3862976190c1934d55cd9b2121
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector--lims-lightgray.png?logo=github
:target: https://github.com/OCA/connector-lims/tree/18.0/lims_stock
:alt: OCA/connector-lims
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/connector-lims-18-0/connector-lims-18-0-lims_stock
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/connector-lims&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Integrates **LIMS** with **Odoo Inventory** to track specimens as
serialized stock items.

Features
~~~~~~~~

- Adds *Is Specimen* field on products; sets variants to serial
tracking.
- Links specimens to products, lots/serials, and current stock
locations.
- Auto-creates serial numbers for specimen products if missing.
- Validates that selected lots belong to the same product.
- Shows current specimen location (from stock quants).
- “Open Lot” button to view related serial record.

**Table of contents**

.. contents::
:local:

Configuration
=============

1. Install **lims_stock** after ``lims`` and ``stock``.
2. Go to **Inventory › Products** → enable **Is Specimen** on templates.
3. Ensure ``lims.specimen`` sequence and LIMS stages exist.
4. (Optional) Give LIMS users access to stock lots if they need to open
them.

Usage
=====

1. **Create Specimen**

- Open **LIMS › Specimens › New**.
- Choose a specimen-enabled product.
- Leave *Lot/Serial* empty to auto-create one.

2. **View Location**

- The *Current Location* field shows the lot’s stock location.

3. **Open Serial**

- Click **Open Lot** to view the stock lot form.

4. **Inventory Sync**

- Moving the serial in stock automatically updates its location when
reopening the specimen.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/connector-lims/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/connector-lims/issues/new?body=module:%20lims_stock%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Open Source Integrators

Contributors
------------

- Rodrigo Madrid rmadrid@opensourceintegrators.com
- Adriana Alpizar aalpizar@opensourceintegrators.com
- Maxime Chambreuil mchambreuil@opensourceintegrators.com
- Hardik Suthar hsuthar@opensourceintegrators.com

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/connector-lims <https://github.com/OCA/connector-lims/tree/18.0/lims_stock>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions lims_stock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions lims_stock/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "LIMS Stock",
"version": "18.0.1.0.0",
"summary": "Track specimens using stock (lots/location) for LIMS",
"author": "Open Source Integrators, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/connector-lims",
"license": "AGPL-3",
"category": "LIMS",
"depends": ["lims", "stock", "product"],
"data": [
"views/product_template_views.xml",
"views/lims_specimen_views.xml",
],
"installable": True,
"application": False,
}
2 changes: 2 additions & 0 deletions lims_stock/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import product_template
from . import lims_specimen
133 changes: 133 additions & 0 deletions lims_stock/models/lims_specimen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright (C) 2025 Open Source Integrators
# License AGPL-3.0 or later
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class LIMSSpecimen(models.Model):
_inherit = "lims.specimen"

# NEW fields for lims_stock
product_id = fields.Many2one(
"product.product",
string="Product",
index=True,
help="Product representing the specimen (variant).",
)
lot_id = fields.Many2one(
"stock.lot",
string="Lot / Serial",
index=True,
help="Lot / Serial that represents this specimen.",
)
location_id = fields.Many2one(
"stock.location",
string="Current Location",
compute="_compute_location_id",
store=False,
help="Computed from the lot's stock.quant location (if available).",
)

@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
# After create: ensure lot creation/validation for specimen products
for rec in records:
rec._ensure_lot_for_specimen()
return records

def write(self, vals):
res = super().write(vals)
# Recompute location for records touched
self.filtered(lambda r: r.lot_id)._compute_location_id()
# If product_id changed for any record we might ensure lot validity
if "product_id" in vals:
for rec in self:
rec._ensure_lot_for_specimen()
return res

def _ensure_lot_for_specimen(self):
"""If product is serial tracked and lot not provided,
create a lot/serial automatically."""
for rec in self:
product = rec.product_id
if not product:
continue
# check product template flag or product tracking
is_specimen_flag = (
product.product_tmpl_id.is_specimen
if product.product_tmpl_id
else False
)
serial_tracked = (product.tracking == "serial") or is_specimen_flag
if serial_tracked and not rec.lot_id:
# create a new lot/serial. Use specimen name or sequence for lot name.
lot_name = (
rec.name
or self.env["ir.sequence"].next_by_code("stock.lot")
or None
)
lot_vals = {
"product_id": product.id,
"name": lot_name or rec.name,
}
new_lot = self.env["stock.lot"].create(lot_vals)
rec.lot_id = new_lot

# if lot exists, validate product match
if rec.lot_id and rec.product_id:
lot_product = getattr(rec.lot_id, "product_id", False)
if lot_product and lot_product.id != rec.product_id.id:
raise ValidationError(
_(
f"""Selected lot {rec.lot_id.name} is not for
product {rec.product_id.display_name}"""
)
)

@api.depends("lot_id")
def _compute_location_id(self):
"""Safely compute location without recursive re-entry."""
# Prevent recursion loop if already computing this in current call stack
if self.env.context.get("lims_location_recursion_guard"):
return

StockQuant = self.env["stock.quant"]
for rec in self.with_context(lims_location_recursion_guard=True):
location = False
try:
if rec.lot_id:
quant = (
StockQuant.sudo()
.with_context(active_test=False)
.search([("lot_id", "=", rec.lot_id.id)], limit=1)
)
if quant:
location = quant.location_id
except Exception:
# swallow errors to prevent next_stage from breaking
location = False

# assign value silently (no write)
object.__setattr__(rec, "location_id", location)

# helper action to open the lot record

def action_open_lot(self):
"""Open the Stock Lot (Serial Number) form view for this specimen."""
self.ensure_one()
if not self.lot_id:
return {"type": "ir.actions.act_window_close"}

# Use the canonical action for stock lots/serials
action = self.env.ref("stock.action_product_production_lot_form").read()[0]
action.update(
{
"name": "Lot / Serial",
"view_mode": "form",
"res_id": self.lot_id.id,
"views": [(False, "form")],
"target": "current",
}
)
return action
38 changes: 38 additions & 0 deletions lims_stock/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright (C) 2025 Open Source Integrators
# License AGPL-3.0 or later
from odoo import api, fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"

is_specimen = fields.Boolean(
default=False,
help="""If checked, this product's variants will be tracked by
serial number for specimens.""",
)

@api.model_create_multi
def create(self, vals_list):
recs = super().create(vals_list)
# set tracking='serial' on variants if is_specimen true
to_update = recs.filtered(lambda r: r.is_specimen)
if to_update:
for tmpl in to_update:
if tmpl.product_variant_ids:
tmpl.product_variant_ids.sudo().write(
{"tracking": "serial", "is_storable": True}
)
return recs

def write(self, vals):
res = super().write(vals)
# if is_specimen changed to True on any template, enforce variant tracking
if "is_specimen" in vals:
templates = self.filtered(lambda t: t.is_specimen)
for tmpl in templates:
if tmpl.product_variant_ids:
tmpl.product_variant_ids.sudo().write(
{"tracking": "serial", "is_storable": True}
)
return res
3 changes: 3 additions & 0 deletions lims_stock/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
4 changes: 4 additions & 0 deletions lims_stock/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1. Install **lims_stock** after `lims` and `stock`.
2. Go to **Inventory › Products** → enable **Is Specimen** on templates.
3. Ensure `lims.specimen` sequence and LIMS stages exist.
4. (Optional) Give LIMS users access to stock lots if they need to open them.
4 changes: 4 additions & 0 deletions lims_stock/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Rodrigo Madrid <rmadrid@opensourceintegrators.com>
- Adriana Alpizar <aalpizar@opensourceintegrators.com>
- Maxime Chambreuil <mchambreuil@opensourceintegrators.com>
- Hardik Suthar <hsuthar@opensourceintegrators.com>
9 changes: 9 additions & 0 deletions lims_stock/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Integrates **LIMS** with **Odoo Inventory** to track specimens as serialized stock items.

### Features
- Adds *Is Specimen* field on products; sets variants to serial tracking.
- Links specimens to products, lots/serials, and current stock locations.
- Auto-creates serial numbers for specimen products if missing.
- Validates that selected lots belong to the same product.
- Shows current specimen location (from stock quants).
- “Open Lot” button to view related serial record.
10 changes: 10 additions & 0 deletions lims_stock/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
1. **Create Specimen**
- Open **LIMS › Specimens › New**.
- Choose a specimen-enabled product.
- Leave *Lot/Serial* empty to auto-create one.
2. **View Location**
- The *Current Location* field shows the lot’s stock location.
3. **Open Serial**
- Click **Open Lot** to view the stock lot form.
4. **Inventory Sync**
- Moving the serial in stock automatically updates its location when reopening the specimen.
Loading
Loading