Odoo18-Base/addons/repair/tests/test_repair.py

855 lines
38 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import tagged, common, Form
from odoo.tools import float_compare, float_is_zero
@tagged('post_install', '-at_install')
class TestRepair(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Partners
cls.res_partner_1 = cls.env['res.partner'].create({'name': 'Wood Corner'})
cls.res_partner_address_1 = cls.env['res.partner'].create({'name': 'Willie Burke', 'parent_id': cls.res_partner_1.id})
cls.res_partner_12 = cls.env['res.partner'].create({'name': 'Partner 12'})
# Products
cls.product_product_3 = cls.env['product.product'].create({'name': 'Desk Combination'})
cls.product_product_11 = cls.env['product.product'].create({
'name': 'Conference Chair',
'lst_price': 30.0,
})
cls.product_product_5 = cls.env['product.product'].create({'name': 'Product 5'})
cls.product_product_6 = cls.env['product.product'].create({'name': 'Large Cabinet'})
cls.product_product_12 = cls.env['product.product'].create({'name': 'Office Chair Black'})
cls.product_product_13 = cls.env['product.product'].create({'name': 'Corner Desk Left Sit'})
# Storable products
cls.product_storable_no = cls.env['product.product'].create({
'name': 'Product Storable No Tracking',
'type': 'product',
'tracking': 'none',
})
cls.product_storable_serial = cls.env['product.product'].create({
'name': 'Product Storable Serial',
'type': 'product',
'tracking': 'serial',
})
cls.product_storable_lot = cls.env['product.product'].create({
'name': 'Product Storable Lot',
'type': 'product',
'tracking': 'lot',
})
# 'Create Repair' Products
cls.product_consu_order_repair = cls.env['product.product'].create({
'name': 'Repair Consumable',
'type': 'consu',
'create_repair': True,
})
cls.product_storable_order_repair = cls.env['product.product'].create({
'name': 'Repair Storable',
'type': 'product',
'create_repair': True,
})
cls.product_service_order_repair = cls.env['product.product'].create({
'name': 'Repair Service',
'type': 'service',
'create_repair': True,
})
# Location
cls.stock_warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.env.company.id)], limit=1)
cls.stock_location_14 = cls.env['stock.location'].create({
'name': 'Shelf 2',
'location_id': cls.stock_warehouse.lot_stock_id.id,
})
# Repair Orders
cls.repair1 = cls.env['repair.order'].create({
'product_id': cls.product_product_3.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'product_id': cls.product_product_11.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
'partner_id': cls.res_partner_12.id,
})
cls.repair0 = cls.env['repair.order'].create({
'product_id': cls.product_product_5.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'user_id': False,
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'product_id': cls.product_product_12.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
'partner_id': cls.res_partner_12.id,
})
cls.repair2 = cls.env['repair.order'].create({
'product_id': cls.product_product_6.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'user_id': False,
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'product_id': cls.product_product_13.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
'partner_id': cls.res_partner_12.id,
})
cls.env.user.groups_id |= cls.env.ref('stock.group_stock_user')
def _create_simple_repair_order(self):
product_to_repair = self.product_product_5
return self.env['repair.order'].create({
'product_id': product_to_repair.id,
'product_uom': product_to_repair.uom_id.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'partner_id': self.res_partner_12.id
})
def _create_simple_part_move(self, repair_id=False, qty=0.0, product=False):
if not product:
product = self.product_product_5
return self.env['stock.move'].create({
'repair_line_type': 'add',
'product_id': product.id,
'product_uom_qty': qty,
'repair_id': repair_id,
'company_id': self.env.company.id,
})
@classmethod
def create_quant(cls, product, qty, offset=0, name="L"):
i = 1
if product.tracking == 'serial':
i, qty = qty, 1
if name == "L":
name = "S"
vals = []
for x in range(1, i + 1):
qDict = {
'location_id': cls.stock_warehouse.lot_stock_id.id,
'product_id': product.id,
'inventory_quantity': qty,
}
if product.tracking != 'none':
qDict['lot_id'] = cls.env['stock.lot'].create({
'name': name + str(offset + x),
'product_id': product.id,
'company_id': cls.env.company.id
}).id
vals.append(qDict)
return cls.env['stock.quant'].create(vals)
def test_01_repair_states_transition(self):
repair = self._create_simple_repair_order()
# Draft -> Confirmed -> Cancel -> Draft -> Done -> Failing Cancel
# draft -> confirmed (action_validate -> _action_repair_confirm)
# PRE
# lines' qty >= 0 !-> UserError
# product's qty IS available !-> Warning w/ choice
# POST
# state = confirmed
# move_ids in (partially reserved, fully reserved, waiting availability)
# Line A with qty < 0 --> UserError
lineA = self._create_simple_part_move(repair.id, -1.0, self.product_storable_no)
repair.move_ids |= lineA
with self.assertRaises(UserError):
repair.action_validate()
# Line A with qty > 0 & not available, Line B with qty >= 0 & available --> Warning (stock.warn.insufficient.qty.repair)
lineA.product_uom_qty = 2.0
lineB = self._create_simple_part_move(repair.id, 2.0, self.product_storable_lot)
repair.move_ids |= lineB
quant = self.create_quant(self.product_storable_no, 1)
quant |= self.create_quant(self.product_storable_lot, 3)
quant.action_apply_inventory()
lineC = self._create_simple_part_move(repair.id, 1.0, self.product_storable_order_repair)
repair.move_ids |= lineC
repair.product_id = self.product_storable_serial
validate_action = repair.action_validate()
self.assertEqual(validate_action.get("res_model"), "stock.warn.insufficient.qty.repair")
# Warn qty Wizard only apply to "product TO repair"
warn_qty_wizard = Form(
self.env['stock.warn.insufficient.qty.repair']
.with_context(**validate_action['context'])
).save()
warn_qty_wizard.action_done()
self.assertEqual(repair.state, "confirmed", 'Repair order should be in "Confirmed" state.')
self.assertEqual(lineA.state, "partially_available", 'Repair line #1 should be in "Partial Availability" state.')
self.assertEqual(lineB.state, "assigned", 'Repair line #2 should be in "Available" state.')
self.assertEqual(lineC.state, "confirmed", 'Repair line #3 should be in "Waiting Availability" state.')
# Create quotation
# No partner warning -> working case -> already linked warning
# Ensure SO doesn't exist
self.assertEqual(len(repair.sale_order_id), 0)
repair.partner_id = None
with self.assertRaises(UserError) as err:
repair.action_create_sale_order()
self.assertIn("You need to define a customer", err.exception.args[0])
repair.partner_id = self.res_partner_12.id
repair.action_create_sale_order()
# Ensure SO and SOL were created
self.assertNotEqual(len(repair.sale_order_id), 0)
self.assertEqual(len(repair.sale_order_id.order_line), 3)
with self.assertRaises(UserError) as err:
repair.action_create_sale_order()
# (*) -> cancel (action_repair_cancel)
# PRE
# state != done !-> UserError (cf. end of this test)
# POST
# moves_ids state == cancelled
# 'Lines" SOL product_uom_qty == 0
# state == cancel
self.assertNotEqual(repair.state, "done")
repair.action_repair_cancel()
self.assertEqual(repair.state, "cancel")
self.assertTrue(all(m.state == "cancel" for m in repair.move_ids))
self.assertTrue(all(float_is_zero(sol.product_uom_qty, 2) for sol in repair.sale_order_id.order_line))
# (*)/cancel -> draft (action_repair_cancel_draft)
# PRE
# state == cancel !-> action_repair_cancel()
# state != done !~> UserError (transitive..., don't care)
# POST
# move_ids.state == draft
# state == draft
repair.action_repair_cancel_draft()
self.assertEqual(repair.state, "draft")
self.assertTrue(all(m.state == "draft" for m in repair.move_ids))
# draft -> confirmed
# Enforce product_id availability to skip warning
quant = self.create_quant(self.product_storable_serial, 1)
quant.action_apply_inventory()
repair.lot_id = quant.lot_id
repair.action_validate()
self.assertEqual(repair.state, "confirmed")
# confirmed -> under_repair (action_repair_start)
# Purely informative state
repair.action_repair_start()
self.assertEqual(repair.state, "under_repair")
# under_repair -> done (action_repair_end -> action_repair_done)
# PRE
# state == under_repair !-> UserError
# lines' quantity >= lines' product_uom_qty !-> Warning
# line tracked => line has lot_ids !-> ValidationError
# POST
# lines with quantity == 0 are cancelled (related sol product_uom_qty is consequently set to 0)
# repair.product_id => repair.move_id
# move_ids.state == (done || cancel)
# state == done
# move_ids with quantity (LOWER or HIGHER than) product_uom_qty MUST NOT be splitted
# Any line with quantity < product_uom_qty => Warning
repair.move_ids.picked = True
end_action = repair.action_repair_end()
self.assertEqual(end_action.get("res_model"), "repair.warn.uncomplete.move")
warn_uncomplete_wizard = Form(
self.env['repair.warn.uncomplete.move']
.with_context(**end_action['context'])
).save()
# LineB : no serial => ValidationError
lot = lineB.move_line_ids.lot_id
with self.assertRaises(UserError) as err:
lineB.move_line_ids.lot_id = False
warn_uncomplete_wizard.action_validate()
# LineB with lots
lineB.move_line_ids.lot_id = lot
lineA.quantity = 2 # quantity = product_uom_qty
lineC.quantity = 2 # quantity > product_uom_qty (No warning)
lineD = self._create_simple_part_move(repair.id, 0.0)
repair.move_ids |= lineD # product_uom_qty = 0 : state is cancelled
self.assertEqual(lineD.state, 'assigned')
num_of_lines = len(repair.move_ids)
self.assertFalse(repair.move_id)
end_action = repair.action_repair_end()
self.assertEqual(end_action.get("res_model"), "repair.warn.uncomplete.move")
warn_uncomplete_wizard = Form(
self.env['repair.warn.uncomplete.move']
.with_context(**end_action['context'])
).save()
warn_uncomplete_wizard.action_validate()
self.assertFalse((repair.move_id | repair.move_ids).picking_id, "No picking for repair moves")
self.assertEqual(repair.state, "done")
done_moves = repair.move_ids - lineD
#line a,b,c are 'done', line d is 'cancel'
self.assertTrue(all(m.state == 'done' for m in done_moves))
self.assertEqual(lineD.state, 'cancel')
self.assertEqual(len(repair.move_id), 1)
self.assertEqual(len(repair.move_ids), num_of_lines) # No split
# (*) -> cancel (action_repair_cancel)
# PRE
# state != done !-> UserError
with self.assertRaises(UserError) as err:
repair.action_repair_cancel()
def test_02_repair_sale_order_binding(self):
# Binding from SO to RO(s)
# On SO Confirm
# - Create linked RO per line (only if item with "create_repair" checked)
# Create Repair SOL
# - sol qty updated to 0 -> RO canceled (Reciprocal is true too)
# - sol qty back to >0 -> RO Confirmed (Reciprocal is not true)
# RO Parts SOL
# - SOL qty change is NOT propagated to RO
# - However, these changes FROM RO are propagated to SO
#----------------------------------------------------------------------------------
# Binding from RO to SO
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.res_partner_1
with so_form.order_line.new() as line:
line.product_id = self.product_consu_order_repair
line.product_uom_qty = 2.0
with so_form.order_line.new() as line:
line.display_type = 'line_section'
line.name = 'Dummy Section'
sale_order = so_form.save()
order_line = sale_order.order_line[0]
line_section = sale_order.order_line[1]
self.assertEqual(len(sale_order.repair_order_ids), 0)
sale_order.action_confirm()
# Quantity set on the "create repair" product doesn't affect the number of RO created
self.assertEqual(len(sale_order.repair_order_ids), 1)
repair_order = sale_order.repair_order_ids[0]
self.assertEqual(sale_order, repair_order.sale_order_id)
self.assertEqual(repair_order.state, 'confirmed')
order_line.product_uom_qty = 0
self.assertEqual(repair_order.state, 'cancel')
order_line.product_uom_qty = 1
line_section.name = 'updated section'
self.assertEqual(repair_order.state, 'confirmed')
repair_order.action_repair_cancel()
self.assertTrue(float_is_zero(order_line.product_uom_qty, 2))
order_line.product_uom_qty = 3
self.assertEqual(repair_order.state, 'confirmed')
# Add RO line
ro_form = Form(repair_order)
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'add'
ro_line_form.product_id = self.product_product_11
ro_line_form.product_uom_qty = 1
ro_form.save()
ro_line_0 = repair_order.move_ids[0]
sol_part_0 = ro_line_0.sale_line_id
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# chg qty in SO -> No effect on RO
sol_part_0.product_uom_qty = 5
self.assertNotEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# chg qty in RO -> Update qty in SO
ro_line_0.product_uom_qty = 3
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# with/without warranty
self.assertFalse(float_is_zero(sol_part_0.price_unit, 2))
repair_order.under_warranty = True
self.assertTrue(float_is_zero(sol_part_0.price_unit, 2))
repair_order.under_warranty = False
self.assertFalse(float_is_zero(sol_part_0.price_unit, 2))
# stock_move transitions
# add -> remove -> add -> recycle -> add transitions
ro_line_0.repair_line_type = 'remove'
self.assertTrue(float_is_zero(sol_part_0.product_uom_qty, 2))
ro_line_0.repair_line_type = 'add'
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
ro_line_0.repair_line_type = 'recycle'
self.assertTrue(float_is_zero(sol_part_0.product_uom_qty, 2))
ro_line_0.repair_line_type = 'add'
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# remove and recycle line : not added to SO.
sol_count = len(sale_order.order_line)
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'remove'
ro_line_form.product_id = self.product_product_12
ro_line_form.product_uom_qty = 1
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'recycle'
ro_line_form.product_id = self.product_product_13
ro_line_form.product_uom_qty = 1
ro_form.save()
ro_line_1 = repair_order.move_ids[1]
self.assertEqual(len(sale_order.order_line), sol_count)
# remove to add -> added to SO
ro_line_1.repair_line_type = 'add'
sol_part_1 = ro_line_1.sale_line_id
self.assertNotEqual(len(sale_order.order_line), sol_count)
self.assertEqual(float_compare(sol_part_1.product_uom_qty, ro_line_1.product_uom_qty, 2), 0)
# delete 'remove to add' line in RO -> SOL qty set to 0
repair_order.move_ids = [(2, ro_line_1.id, 0)]
self.assertTrue(float_is_zero(sol_part_1.product_uom_qty, 2))
# repair_order.action_repair_end()
# -> order_line.qty_delivered == order_line.product_uom_qty
# -> "RO Lines"'s SOL.qty_delivered == move.quantity
repair_order.action_repair_start()
for line in repair_order.move_ids:
line.quantity = line.product_uom_qty
repair_order.action_repair_end()
self.assertTrue(float_is_zero(order_line.qty_delivered, 2))
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.quantity, 2), 0)
self.assertTrue(float_is_zero(sol_part_1.qty_delivered, 2))
def test_03_sale_order_delivered_qty(self):
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.res_partner_1
with so_form.order_line.new() as line:
line.product_id = self.product_consu_order_repair
line.product_uom_qty = 1.0
with so_form.order_line.new() as line:
line.product_id = self.product_storable_order_repair
line.product_uom_qty = 1.0
with so_form.order_line.new() as line:
line.product_id = self.product_service_order_repair
line.product_uom_qty = 1.0
sale_order = so_form.save()
sale_order.action_confirm()
repair_order_ids = sale_order.repair_order_ids
repair_order_ids.action_repair_start()
repair_order_ids.action_repair_end()
for sol in sale_order.order_line:
if sol.product_template_id.type == 'service':
self.assertEqual(float_compare(sol.product_uom_qty, sol.qty_delivered, 2), 0)
else:
self.assertTrue(float_is_zero(sol.qty_delivered, 2))
def test_repair_compute_product_uom(self):
repair = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'repair_line_type': 'add',
'product_id': self.product_product_11.id,
})
],
})
self.assertEqual(repair.product_uom, self.product_product_3.uom_id)
self.assertEqual(repair.move_ids[0].product_uom, self.product_product_11.uom_id)
def test_repair_compute_location(self):
repair = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'repair_line_type': 'add',
'product_id': self.product_product_11.id,
})
],
})
self.assertEqual(repair.location_id, self.stock_warehouse.lot_stock_id)
self.assertEqual(repair.move_ids[0].location_id, self.stock_warehouse.lot_stock_id)
location_dest_id = self.env['stock.location'].search([
('usage', '=', 'production'),
('company_id', '=', repair.company_id.id),
], limit=1)
self.assertEqual(repair.move_ids[0].location_dest_id, location_dest_id)
def test_no_recompute_location_when_change_user_after_confirm(self):
user1 = self.env['res.users'].create({
'name': 'A User',
'login': 'a_user',
'email': 'a@user.com',
})
repair_order = self._create_simple_repair_order()
repair_order.location_id = self.stock_location_14
repair_order.recycle_location_id = self.stock_location_14
repair_order.action_validate()
repair_order.user_id = user1
self.assertEqual(repair_order.location_id, self.stock_location_14)
self.assertEqual(repair_order.recycle_location_id, self.stock_location_14)
repair_order.action_repair_start()
repair_order.action_repair_end()
with Form(repair_order) as ro_form:
ro_form.user_id = user1
self.assertEqual(repair_order.location_id, self.stock_location_14)
self.assertEqual(repair_order.recycle_location_id, self.stock_location_14)
def test_purchase_price_so_create_from_repair(self):
"""
Test that the purchase price is correctly set on the SO line,
when creating a SO from a repair order.
"""
if not self.env['ir.module.module'].search([('name', '=', 'sale_margin'), ('state', '=', 'installed')]):
self.skipTest("sale_margin is not installed, so there is no purchase price to test")
self.product_product_11.standard_price = 10
repair = self.env['repair.order'].create({
'partner_id': self.res_partner_1.id,
'product_id': self.product_product_3.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'repair_line_type': 'add',
'product_id': self.product_product_11.id,
})
],
})
repair.action_create_sale_order()
self.assertEqual(repair.sale_order_id.order_line.product_id, self.product_product_11)
self.assertEqual(repair.sale_order_id.order_line.purchase_price, 10)
def test_repair_from_return(self):
"""
create a repair order from a return delivery and ensure that the stock.move
resulting from the repair is not associated with the return picking.
"""
product = self.env['product.product'].create({
'name': 'Test Product',
'type': 'product',
})
self.env['stock.quant']._update_available_quantity(product, self.stock_location_14, 1)
picking_form = Form(self.env['stock.picking'])
#create a delivery order
picking_form.picking_type_id = self.stock_warehouse.out_type_id
picking_form.partner_id = self.res_partner_1
with picking_form.move_ids_without_package.new() as move:
move.product_id = product
move.product_uom_qty = 1.0
picking = picking_form.save()
picking.action_confirm()
picking.action_assign()
picking.button_validate()
self.assertEqual(picking.state, 'done')
# Create a return
stock_return_picking_form = Form(self.env['stock.return.picking']
.with_context(active_ids=picking.ids, active_id=picking.ids[0],
active_model='stock.picking'))
stock_return_picking = stock_return_picking_form.save()
stock_return_picking.product_return_moves.quantity = 1.0
stock_return_picking_action = stock_return_picking.create_returns()
return_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_picking.move_ids.picked = True
return_picking.button_validate()
self.assertEqual(return_picking.state, 'done')
res_dict = return_picking.action_repair_return()
repair_form = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context']), view=res_dict['view_id'])
repair_form.product_id = product
# The repair needs to be saved to ensure the context is correctly set.
repair = repair_form.save()
repair_form = Form(repair)
with repair_form.move_ids.new() as move:
move.product_id = self.product_product_5
move.product_uom_qty = 1.0
move.quantity = 1.0
move.repair_line_type = 'add'
repair = repair_form.save()
repair.action_repair_start()
repair.action_repair_end()
self.assertEqual(repair.state, 'done')
self.assertEqual(len(return_picking.move_ids), 1, "Parts added to the repair order shoudln't be added to the return picking")
self.assertEqual(repair.location_id, return_picking.location_dest_id, "Repair location should have defaulted to return destination location")
self.assertEqual(repair.partner_id, return_picking.partner_id, "Repair customer should have defaulted to return customer")
self.assertEqual(repair.picking_type_id, return_picking.picking_type_id.warehouse_id.repair_type_id)
def test_repair_with_product_in_package(self):
"""
Test That a repair order can be validated when the repaired product is tracked and in a package
"""
self.product_product_3.tracking = 'serial'
self.product_product_3.type = 'product'
# Create two serial numbers
sn_1 = self.env['stock.lot'].create({'name': 'sn_1', 'product_id': self.product_product_3.id})
sn_2 = self.env['stock.lot'].create({'name': 'sn_2', 'product_id': self.product_product_3.id})
# Create two packages
package_1 = self.env['stock.quant.package'].create({'name': 'Package-test-1'})
package_2 = self.env['stock.quant.package'].create({'name': 'Package-test-2'})
# update the quantity of the product in the stock
self.env['stock.quant']._update_available_quantity(self.product_product_3, self.stock_warehouse.lot_stock_id, 1, lot_id=sn_1, package_id=package_1)
self.env['stock.quant']._update_available_quantity(self.product_product_3, self.stock_warehouse.lot_stock_id, 1, lot_id=sn_2, package_id=package_2)
self.assertEqual(self.product_product_3.qty_available, 2)
# create a repair order
repair_order = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'product_uom': self.product_product_3.uom_id.id,
# 'guarantee_limit': '2019-01-01',
'location_id': self.stock_warehouse.lot_stock_id.id,
'lot_id': sn_1.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'product_id': self.product_product_5.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
})
],
})
# Validate and complete the repair order
repair_order.action_validate()
self.assertEqual(repair_order.state, 'confirmed')
repair_order.action_repair_start()
self.assertEqual(repair_order.state, 'under_repair')
repair_order.move_ids.quantity = 1
repair_order.action_repair_end()
self.assertEqual(repair_order.state, 'done')
def test_sn_with_no_tracked_product(self):
"""
Check that the lot_id field is cleared after updating the product in the repair order.
"""
self.env.ref('base.group_user').implied_ids += (
self.env.ref('stock.group_production_lot')
)
sn_1 = self.env['stock.lot'].create({'name': 'sn_1', 'product_id': self.product_storable_serial.id})
ro_form = Form(self.env['repair.order'])
ro_form.product_id = self.product_storable_serial
ro_form.lot_id = sn_1
repair_order = ro_form.save()
ro_form = Form(repair_order)
ro_form.product_id = self.product_storable_no
repair_order = ro_form.save()
self.assertFalse(repair_order.lot_id)
def test_repair_multi_unit_order_with_serial_tracking(self):
"""
Test that a sale order with a single order line with quantity > 1 for a product that creates a repair order and
is tracked via serial number creates multiple repair orders rather than grouping the line into a single RO
"""
product_a = self.env['product.product'].create({
'name': 'productA',
'detailed_type': 'product',
'tracking': 'serial',
'create_repair': True,
})
sale_order = self.env['sale.order'].create({
'partner_id': self.res_partner_1.id,
'order_line': [Command.create({
'product_id': product_a.id,
'product_uom_qty': 3.0,
})]
})
sale_order.action_confirm()
repair_orders = sale_order.repair_order_ids
self.assertRecordValues(repair_orders, [
{'product_id': product_a.id, 'product_qty': 1.0},
{'product_id': product_a.id, 'product_qty': 1.0},
{'product_id': product_a.id, 'product_qty': 1.0},
])
def test_onchange_picking_type_id_and_name(self):
"""
Test that when changing the picking_type_id, the name of the repair order should be changed too
"""
repair_order = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
})
picking_type_1 = self.env['stock.picking.type'].create({
'name': 'new_picking_type_1',
'code': 'repair_operation',
'sequence_code': 'PT1/',
})
picking_type_2 = self.env['stock.picking.type'].create({
'name': 'new_picking_type_2',
'code': 'repair_operation',
'sequence_code': 'PT2/',
})
repair_order.picking_type_id = picking_type_1
self.assertEqual(repair_order.name, "PT1/00001")
repair_order.picking_type_id = picking_type_2
self.assertEqual(repair_order.name, "PT2/00001")
repair_order.picking_type_id = picking_type_1
self.assertEqual(repair_order.name, "PT1/00002")
repair_order.picking_type_id = picking_type_1
self.assertEqual(repair_order.name, "PT1/00002")
def test_repair_components_lots_show_in_invoice(self):
"""
Test that the lots of the components of a repair order are shown in the invoice
"""
quant = self.create_quant(self.product_storable_serial, 1)
quant.action_apply_inventory()
repair_order = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'product_uom': self.product_product_3.uom_id.id,
'partner_id': self.res_partner_12.id,
'move_ids': [
Command.create({
'product_id': self.product_storable_serial.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
})
],
})
repair_order.action_validate()
repair_order.action_repair_start()
repair_order.action_repair_end()
repair_order.action_create_sale_order()
sale_order = repair_order.sale_order_id
sale_order.action_confirm()
invoice = sale_order._create_invoices()
invoice.action_post()
res = invoice._get_invoiced_lot_values()
self.assertEqual(len(res), 1, "The invoice should have one line")
self.assertEqual(res[0]['product_name'], self.product_storable_serial.display_name, "The product name should be the same")
self.assertEqual(res[0]['lot_name'], quant.lot_id.name, "The lot name should be the same")
def test_trigger_orderpoint_from_repair(self):
"""
Test that the order point triggered by the repair order creates a move linked to a picking.
"""
self.assertFalse(self.env['stock.move'].search([('product_id', '=', self.product_storable_no.id)]))
route = self.env['stock.route'].create({
'name': 'new route',
'rule_ids': [(0, False, {
'name': 'rule_test',
'location_src_id': self.stock_warehouse.lot_stock_id.id,
'location_dest_id': self.stock_location_14.id,
'company_id': self.env.company.id,
'action': 'pull',
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'procure_method': 'make_to_stock'
})],
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'Cake RR',
'product_id': self.product_storable_no,
'route_id': route.id,
'location_id': self.stock_location_14.id,
'product_id': self.product_storable_no.id,
'product_min_qty': 0,
'product_max_qty': 1,
'trigger': 'auto'
})
# The product to be repaired should be storable and out of stock
# to trigger the wizard indicating that the product has an insufficient quantity.
self.product_product_3.type = 'product'
repair_order = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'product_uom': self.product_product_3.uom_id.id,
'partner_id': self.res_partner_12.id,
'location_id': self.stock_location_14.id,
'move_ids': [
Command.create({
'product_id': self.product_storable_no.id,
'product_uom_qty': 1.0,
'state': 'draft',
'repair_line_type': 'add',
})
],
})
validate_action = repair_order.action_validate()
self.assertEqual(validate_action.get("res_model"), "stock.warn.insufficient.qty.repair")
warn_qty_wizard = Form(
self.env['stock.warn.insufficient.qty.repair']
.with_context(**validate_action['context'])
).save()
warn_qty_wizard.action_done()
self.assertEqual(repair_order.state, "confirmed", 'Repair order should be in "Confirmed" state.')
move = self.env['stock.move'].search([
('product_id', '=', self.product_storable_no.id),
('location_dest_id', '=', self.stock_location_14.id,)
])
self.assertTrue(move.picking_id)
self.assertFalse(move.repair_id)
self.assertEqual(move.location_id, self.stock_warehouse.lot_stock_id)
self.assertEqual(move.location_dest_id, self.stock_location_14)
def test_open_and_create_repair_from_lot(self):
"""
Test that the repair order can be opened from the lot and that it is created correctly.
"""
sn_1 = self.env['stock.lot'].create({'name': 'sn_1', 'product_id': self.product_storable_serial.id})
action = sn_1.action_lot_open_repairs()
context = action.get('context')
tracked_product_repair_line = self.env['product.product'].create({
'name': 'Test Product',
'type': 'product',
'tracking': 'serial',
})
tracked_product_sn = self.env['stock.lot'].create({'name': 'tracked_product_sn1', 'product_id': tracked_product_repair_line.id})
repair_order = self.env['repair.order'].with_context(context).create({
'product_id': self.product_storable_serial.id,
'product_uom': self.product_storable_serial.uom_id.id,
'location_id': self.stock_warehouse.lot_stock_id.id,
'lot_id': sn_1.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
})
repair_order.with_context(context).move_ids = [Command.create({
'product_id': tracked_product_repair_line.id,
'product_uom_qty': 1.0,
'repair_line_type': 'add',
'lot_ids': [(4, tracked_product_sn.id)],
'quantity': 1.0,
})]
self.assertEqual(repair_order.lot_id, sn_1)
def test_copy_repair_product_with_different_groups(self):
"""
This test checks if the product can be copied with users that don't have access on the Inventory app
"""
product_templ = self.env['product.template'].create({
'name': "Repair Consumable",
'type': 'consu',
'create_repair': True,
})
mitchell_user = self.env['res.users'].create({
'name': "Mitchell not Admin",
'login': "m_user",
'email': "m@user.com",
'groups_id': [Command.set(self.env.ref('sales_team.group_sale_manager').ids)],
})
product_templ.invalidate_recordset(['create_repair'])
with self.assertRaises(AccessError):
product_templ.with_user(mitchell_user).create_repair
copied_without_access = product_templ.with_user(mitchell_user).copy()
mitchell_user.write({
'groups_id': [Command.link(self.env.ref('stock.group_stock_user').id)]
})
self.assertFalse(copied_without_access.create_repair)
copied_with_access = product_templ.copy().with_user(mitchell_user)
self.assertTrue(copied_with_access.create_repair)