[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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)]}"/>
|
||||
|
||||
Reference in New Issue
Block a user