[IMP] stock, stock_repair: various features

add the following features:
- add location in form , kanban and list view. Change location creates move
- add smart button for repairs
- add next activity widget in list and kanban
- from the lots/SN smartbutton on the product form, open the kanban view
- show the last delivery partner on serial

closes odoo/odoo#117316

Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
snd
2023-06-09 13:49:40 +02:00
parent 0beb6608c2
commit c57f9efe83
9 changed files with 160 additions and 6 deletions
+17
View File
@@ -8,6 +8,7 @@ class StockLot(models.Model):
repair_order_ids = fields.Many2many('repair.order', string="Repair Orders", compute="_compute_repair_order_ids")
repair_order_count = fields.Integer('Repair order count', compute="_compute_repair_order_ids")
repaired_count = fields.Integer('Repaired count', compute='_compute_repaired_count')
@api.depends('name')
def _compute_repair_order_ids(self):
@@ -18,6 +19,22 @@ class StockLot(models.Model):
lot.repair_order_ids = repair_orders[lot.id]
lot.repair_order_count = len(lot.repair_order_ids)
def _compute_repaired_count(self):
for lot in self:
lot.repaired_count = len(self.env['repair.order'].search([('lot_id', '=', self.id), ('state', '=', 'done')]))
def action_lot_open_repairs(self):
action = self.env["ir.actions.actions"]._for_xml_id("repair.action_repair_order_tree")
action.update({
'domain': [('lot_id', '=', self.id)],
'context': {
'default_product_id': self.product_id.id,
'default_lot_id': self.id,
'default_company_id': self.company_id.id,
},
})
return action
def action_view_ro(self):
self.ensure_one()
+10 -1
View File
@@ -11,8 +11,17 @@
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="repair_order_count" widget="statinfo" nolabel="1" class="mr4"/>
<span class="o_stat_text">Used</span>
</span>
<span class="o_stat_text">Repairs</span>
<span class="o_stat_text">In Repairs</span>
</div>
</button>
<button name="action_lot_open_repairs" icon="fa-wrench" class="oe_stat_button" type="object">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="repaired_count" widget="statinfo" nolabel="1" class="mr4"/>
</span>
<span class="o_stat_text">Repaired</span>
</div>
</button>
</xpath>
+4 -2
View File
@@ -499,12 +499,13 @@ class Product(models.Model):
def action_open_product_lot(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_production_lot_form")
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_product_production_lot_form")
action['domain'] = [('product_id', '=', self.id)]
action['context'] = {
'default_product_id': self.id,
'set_product_readonly': True,
'default_company_id': (self.company_id or self.env.company).id,
'search_default_group_by_location': True,
}
return action
@@ -944,11 +945,12 @@ class ProductTemplate(models.Model):
def action_open_product_lot(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_production_lot_form")
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_product_production_lot_form")
action['domain'] = [('product_id.product_tmpl_id', '=', self.id)]
action['context'] = {
'default_product_tmpl_id': self.id,
'default_company_id': (self.company_id or self.env.company).id,
'search_default_group_by_location': True,
}
if self.product_variant_count == 1:
action['context'].update({
+14
View File
@@ -34,6 +34,7 @@ class StockLot(models.Model):
delivery_count = fields.Integer('Delivery order count', compute='_compute_delivery_ids')
last_delivery_partner_id = fields.Many2one('res.partner', compute='_compute_delivery_ids')
lot_properties = fields.Properties('Properties', definition='product_id.lot_properties_definition', copy=True)
location_id = fields.Many2one('stock.location', 'Location', compute='_compute_single_location', store=True, readonly=False, inverse='_set_single_location', domain="[('usage', '!=', 'view')]")
@api.model
def generate_lot_names(self, first_lot, count):
@@ -126,6 +127,19 @@ class StockLot(models.Model):
if lot.product_id.tracking == 'serial' and lot.delivery_count > 0:
lot.last_delivery_partner_id = lot.delivery_ids.sorted(key=attrgetter('date_done'), reverse=True)[0].partner_id
@api.depends('quant_ids')
def _compute_single_location(self):
for lot in self:
location_id = lot.quant_ids.filtered(lambda q: q.quantity > 0).mapped('location_id')
lot.location_id = location_id if len(location_id) == 1 else False
def _set_single_location(self):
quant_to_move = self.quant_ids.filtered(lambda q: q.quantity > 0)
if len(quant_to_move) == 1:
quant_to_move._move_quants(self.location_id, 'Lot/Serial Number Relocated')
elif len(quant_to_move) > 1:
raise UserError(_('You can only move a lot/serial to a new location if it exists in a single location.'))
@api.model_create_multi
def create(self, vals_list):
self._check_create()
+12 -1
View File
@@ -77,7 +77,6 @@ class StockQuant(models.Model):
'stock.lot', 'Lot/Serial Number', index=True,
ondelete='restrict', check_company=True,
domain=lambda self: self._domain_lot_id())
lot_properties = fields.Properties(related='lot_id.lot_properties', definition='product_id.lot_properties_definition', readonly=True)
sn_duplicated = fields.Boolean(string="Duplicated Serial Number", compute='_compute_sn_duplicated', help="If the same SN is in another Quant")
package_id = fields.Many2one(
'stock.quant.package', 'Package',
@@ -1237,6 +1236,18 @@ class StockQuant(models.Model):
lot_id.name, source_location_id.display_name, ', '.join(sn_locations.mapped('display_name')))
return message, recommended_location
def _move_quants(self, location_dest_id=False, message=False):
""" Directly move a stock.quant to another location by creating a stock.move. """
if not location_dest_id:
return
for quant in self:
move_vals = quant._get_inventory_move_values(quant.quantity, quant.location_id, location_dest_id)
move_vals.update({
'name': message or 'Quantity Relocated',
})
moves = self.env['stock.move'].with_context(inventory_mode=False).create(move_vals)
moves._action_done()
class QuantPackage(models.Model):
""" Packages containing quants and/or other packages """
+1
View File
@@ -21,3 +21,4 @@ from . import test_report
from . import test_report_stock_quantity
from . import test_report_tours
from . import test_stock_return_picking
from . import test_stock_lot
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.tests.common import Form
class TestLotSerial(TestStockCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.locationA = cls.env['stock.location'].create({
'name': 'Location A',
'usage': 'internal',
})
cls.locationB = cls.env['stock.location'].create({
'name': 'Location B',
'usage': 'internal',
})
cls.locationC = cls.env['stock.location'].create({
'name': 'Location C',
'usage': 'internal',
})
cls.productA.tracking = 'lot'
cls.lot_p_a = cls.LotObj.create({
'name': 'lot_product_a',
'product_id': cls.productA.id,
'company_id': cls.env.company.id,
})
cls.StockQuantObj.create({
'product_id': cls.productA.id,
'location_id': cls.locationA.id,
'quantity': 10.0,
'lot_id': cls.lot_p_a.id
})
cls.productB.tracking = 'serial'
cls.lot_p_b = cls.LotObj.create({
'name': 'lot_product_b',
'product_id': cls.productB.id,
'company_id': cls.env.company.id,
})
cls.StockQuantObj.create({
'product_id': cls.productB.id,
'location_id': cls.locationA.id,
'quantity': 1.0,
'lot_id': cls.lot_p_b.id
})
def test_single_location(self):
self.assertEqual(self.lot_p_a.location_id, self.locationA)
self.assertEqual(self.lot_p_b.location_id, self.locationA)
# testing changing the location from the lot form
lot_b_form = Form(self.lot_p_b)
lot_b_form.location_id = self.locationB
lot_b_form.save()
self.assertEqual(self.lot_p_b.quant_ids.filtered(lambda q: q.quantity > 0).location_id, self.locationB)
# testing changing the location from the quant
self.lot_p_b.quant_ids._move_quants(self.locationC, 'test_quant_move')
self.assertEqual(self.lot_p_b.location_id, self.locationC)
# testing having the lot in multiple locations
self.StockQuantObj.create({
'product_id': self.productA.id,
'location_id': self.locationC.id,
'quantity': 10.0,
'lot_id': self.lot_p_a.id
})
self.assertEqual(self.lot_p_a.location_id.id, False)
# testing having the lot back in a single location
self.lot_p_a.quant_ids.filtered(lambda q: q.location_id == self.locationA)._move_quants(self.locationC)
self.StockQuantObj.invalidate_model()
self.StockQuantObj._unlink_zero_quants()
self.assertEqual(self.lot_p_a.location_id, self.locationC)
+26 -1
View File
@@ -48,6 +48,8 @@
</div>
<field name="ref"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="location_id" attrs="{'invisible': [('location_id', '=', False)]}" groups="stock.group_stock_manager"/>
<field name="last_delivery_partner_id" attrs="{'invisible': [('last_delivery_partner_id', '=', False)]}" groups="stock.group_stock_manager"/>
</group>
<group></group>
<field name="lot_properties" nolabel="1" columns="2"/>
@@ -78,6 +80,7 @@
<field name="create_date" optional="show"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="last_delivery_partner_id" optional="hide" string="Transfer to"/>
<field name="activity_ids" widget="list_activity" optional="show"/>
<field name="lot_properties" optional="hide"/>
</tree>
</field>
@@ -87,7 +90,7 @@
<field name="name">stock.production.lot.kanban</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<kanban>
<kanban group_create="false" group_delete="false">
<field name="name"/>
<field name="ref"/>
<field name="product_id"/>
@@ -109,6 +112,7 @@
<field name="lot_properties" widget="properties"/>
</div>
</div>
<field name="activity_ids" widget="kanban_activity"/>
</div>
</t>
</templates>
@@ -126,6 +130,7 @@
<field name="lot_properties"/>
<group expand="0" string="Group By">
<filter name="group_by_product" string="Product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter name="group_by_location" string="Location" domain="[]" context="{'group_by': 'location_id'}"/>
</group>
</search>
</field>
@@ -151,6 +156,26 @@
</field>
</record>
<record id="action_product_production_lot_form" model="ir.actions.act_window">
<field name="name">Lots/Serial Numbers</field>
<field name="res_model">stock.lot</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_production_lot_kanban')}),
(0, 0, {'view_mode': 'tree', 'view_id': ref('view_production_lot_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('view_production_lot_form')})]"/>
<field name="search_view_id" ref="search_product_lot_filter"/>
<field name="context">{'search_default_group_by_product': 1, 'display_complete': True, 'default_company_id': allowed_company_ids[0]}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a lot/serial number
</p><p>
Lots/Serial numbers help you tracking the path followed by your products.
From their traceability report you will see the full history of their use, as well as their composition.
</p>
</field>
</record>
<menuitem action="action_production_lot_form" id="menu_action_production_lot_form"
parent="menu_stock_inventory_control" groups="stock.group_production_lot"
sequence="101"/>
-1
View File
@@ -449,7 +449,6 @@ in this location. That leads to a negative stock.
<field name="inventory_date" optional="show"/>
<field name="user_id" string="User" optional="show"/>
<field name='company_id' groups="base.group_multi_company" optional="hide"/>
<field name="lot_properties" optional="hide"/>
<button name="action_inventory_history" type="object" class="btn btn-link text-info" icon="fa-history" string="History"/>
<button name="action_apply_inventory" groups="stock.group_stock_manager" type="object" string="Apply" class="btn btn-link" icon="fa-save" attrs="{'invisible': [('inventory_quantity_set', '=', False)]}"/>
<button name="action_set_inventory_quantity" type="object" string="Set" class="btn btn-link" icon="fa-bullseye" attrs="{'invisible': [('inventory_quantity_set', '=', True)]}"/>