# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command from odoo.tests import common, tagged, Form from odoo.tools import mute_logger from datetime import datetime class TestDropship(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.dropshipping_route = cls.env.ref('stock_dropshipping.route_drop_shipping') cls.supplier = cls.env['res.partner'].create({'name': 'Vendor'}) cls.customer = cls.env['res.partner'].create({'name': 'Customer'}) # dropship route to be added in test cls.dropship_product = cls.env['product.product'].create({ 'name': "Pen drive", 'is_storable': True, 'categ_id': cls.env.ref('product.product_category_1').id, 'lst_price': 100.0, 'standard_price': 0.0, 'uom_id': cls.env.ref('uom.product_uom_unit').id, 'uom_po_id': cls.env.ref('uom.product_uom_unit').id, 'seller_ids': [(0, 0, { 'delay': 1, 'partner_id': cls.supplier.id, 'min_qty': 2.0 })], }) cls.lot_dropship_product = cls.env['product.product'].create({ 'name': "Serial product", 'tracking': 'lot', 'seller_ids': [(0, 0, { 'partner_id': cls.supplier.id, })], 'route_ids': [(4, cls.dropshipping_route.id, 0)] }) def test_change_qty(self): # enable the dropship route on the product self.dropship_product.write({'route_ids': [(6, 0, [self.dropshipping_route.id])]}) # sell one unit of dropship product so = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'partner_invoice_id': self.customer.id, 'partner_shipping_id': self.customer.id, 'order_line': [(0, 0, { 'name': self.dropship_product.name, 'product_id': self.dropship_product.id, 'product_uom_qty': 1.00, 'product_uom': self.dropship_product.uom_id.id, 'price_unit': 12, })], 'picking_policy': 'direct', }) so.action_confirm() po = self.env['purchase.order'].search([('group_id', '=', so.procurement_group_id.id)]) po_line = po.order_line # Check dropship count on SO and PO self.assertEqual(po.incoming_picking_count, 0) self.assertEqual(so.delivery_count, 0) # Check the qty on the P0 self.assertAlmostEqual(po_line.product_qty, 1.00) # Update qty on SO and check PO so.write({'order_line': [[1, so.order_line.id, {'product_uom_qty': 2.00}]]}) self.assertAlmostEqual(po_line.product_qty, 2.00) # Create a new so line sol2 = self.env['sale.order.line'].create({ 'order_id': so.id, 'product_id': self.dropship_product.id, 'product_uom_qty': 3.00, 'price_unit': 12, }) # there is a new line pol2 = po.order_line - po_line # the first line is unchanged self.assertAlmostEqual(po_line.product_qty, 2.00) # the new line matches the new line on the so self.assertAlmostEqual(pol2.product_qty, sol2.product_uom_qty) def test_00_dropship(self): self.dropship_product.description_purchase = "description_purchase" self.dropship_product.description = "internal note" self.dropship_product.description_pickingout = "description_out" # Required for `route_id` to be visible in the view self.env.user.groups_id += self.env.ref('stock.group_adv_location') # Create a sales order with a line of 200 PCE incoming shipment, with route_id drop shipping so_form = Form(self.env['sale.order']) so_form.partner_id = self.customer so_form.payment_term_id = self.env.ref('account.account_payment_term_end_following_month') with mute_logger('odoo.tests.common.onchange'): # otherwise complains that there's not enough inventory and # apparently that's normal according to @jco and @sle with so_form.order_line.new() as line: line.product_id = self.dropship_product line.product_uom_qty = 200 line.price_unit = 1.00 line.route_id = self.dropshipping_route sale_order_drp_shpng = so_form.save() # Confirm sales order sale_order_drp_shpng.action_confirm() # Check the sales order created a procurement group which has a procurement of 200 pieces self.assertTrue(sale_order_drp_shpng.procurement_group_id, 'SO should have procurement group') # Check a quotation was created to a certain vendor and confirm so it becomes a confirmed purchase order purchase = self.env['purchase.order'].search([('partner_id', '=', self.supplier.id)]) self.assertTrue(purchase, "an RFQ should have been created by the scheduler") self.assertIn("description_purchase", purchase.order_line.name) purchase.button_confirm() self.assertEqual(purchase.state, 'purchase', 'Purchase order should be in the approved state') # Check dropship count on SO and PO self.assertEqual(purchase.incoming_picking_count, 0) self.assertEqual(sale_order_drp_shpng.delivery_count, 0) self.assertEqual(sale_order_drp_shpng.dropship_picking_count, 1) self.assertEqual(purchase.dropship_picking_count, 1) # Send the 200 pieces purchase.picking_ids.move_ids.quantity = purchase.picking_ids.move_ids.product_qty purchase.picking_ids.move_ids.picked = True self.assertNotIn("description_purchase", purchase.picking_ids.move_ids.description_picking) purchase.picking_ids.button_validate() # Check one move line was created in Customers location with 200 pieces move_line = self.env['stock.move.line'].search([ ('location_dest_id', '=', self.env.ref('stock.stock_location_customers').id), ('product_id', '=', self.dropship_product.id)]) self.assertEqual(len(move_line.ids), 1, 'There should be exactly one move line') # Check description is not the internal note self.assertNotEqual(move_line.move_id.description_picking, self.dropship_product.description) self.assertEqual(move_line.move_id.description_picking, self.dropship_product.description_pickingout) def test_sale_order_picking_partner(self): """ Test that the partner is correctly set on the picking and the move when the product is dropshipped or not.""" # Create a vendor and a customer supplier_dropship = self.env['res.partner'].create({'name': 'Vendor'}) customer = self.env['res.partner'].create({'name': 'Customer'}) # Create new product without any routes super_product = self.env['product.product'].create({ 'name': "Super product", 'seller_ids': [(0, 0, { 'partner_id': supplier_dropship.id, })], }) # Create a sale order so_form = Form(self.env['sale.order']) so_form.partner_id = customer with so_form.order_line.new() as line: line.product_id = super_product sale_order = so_form.save() # Confirm sale order sale_order.action_confirm() # Check the partner of the related picking and move self.assertEqual(sale_order.picking_ids.partner_id, customer) self.assertEqual(sale_order.picking_ids.move_ids.partner_id, customer) # Add a dropship route to the product super_product.route_ids = [self.env.ref('stock_dropshipping.route_drop_shipping').id] # Create a sale order so_form = Form(self.env['sale.order']) so_form.partner_id = customer with so_form.order_line.new() as line: line.product_id = super_product sale_order = so_form.save() # Confirm sale order sale_order.action_confirm() # Check a quotation was created to a certain vendor and confirm it, so it becomes a confirmed purchase order purchase = self.env['purchase.order'].search([('partner_id', '=', supplier_dropship.id)]) self.assertTrue(purchase, "an RFQ should have been created by the scheduler") purchase.button_confirm() self.assertEqual(purchase.state, 'purchase', 'Purchase order should be in the approved state') # Check the partner of the related picking and move self.assertEqual(sale_order.picking_ids.partner_id, supplier_dropship) self.assertEqual(sale_order.picking_ids.move_ids.partner_id, customer) def test_dropshipped_lot_last_delivery(self): """ Check if the `last_delivery_partner_id` of a `stock.lot` is computed correctly in case the last delivery is a dropship transfer """ # Create a sale order sale_order = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [(0, 0, { 'product_id': self.lot_dropship_product.id })] }) sale_order.action_confirm() # Confirm PO purchase = self.env['purchase.order'].search([('partner_id', '=', self.supplier.id)]) self.assertTrue(purchase, "an RFQ should have been created") purchase.button_confirm() sale_order.picking_ids.move_line_ids.lot_name = '123' sale_order.picking_ids.move_ids.picked = True sale_order.picking_ids.button_validate() self.assertEqual(sale_order.picking_ids.state, 'done') self.assertEqual(sale_order.picking_ids.move_line_ids.lot_id.name, '123') self.assertEqual(sale_order.picking_ids.move_line_ids.lot_id.last_delivery_partner_id, self.customer) def test_sol_reserved_qty_wizard_dropship(self): """ Check that the reserved qty wizard related to a sol is computed from the PO if the product is dropshipped and check that the linked pol is updated. Check that both are again updated when the dropship is returned. """ product = self.dropship_product product.route_ids = self.dropshipping_route sale_order = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [Command.create({ 'product_id': product.id, 'product_uom_qty': 3.0, })] }) sale_order.action_confirm() self.assertEqual(sale_order.order_line.qty_available_today, 0.0) purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.supplier.id)]) purchase_order.button_confirm() picking_dropship = sale_order.picking_ids.filtered(lambda p: p.picking_type_id) self.assertTrue(picking_dropship) self.assertEqual(sale_order.order_line.qty_available_today, 3.0) self.assertRecordValues(sale_order.order_line, [{'qty_available_today': 3.0, 'qty_delivered': 0.0}]) picking_dropship.move_ids.quantity = 3.0 picking_dropship.move_ids.picked = True picking_dropship.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 3.0) self.assertEqual(purchase_order.order_line.qty_received, 3.0) stock_return_picking_form = Form(self.env['stock.return.picking'].with_context( active_ids=picking_dropship.ids, active_id=picking_dropship.id, active_model='stock.picking' )) return_wiz = stock_return_picking_form.save() return_wiz.product_return_moves.quantity = 3 res = return_wiz.action_create_returns() return_picking = self.env['stock.picking'].browse(res['res_id']) return_picking.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 0) self.assertEqual(purchase_order.order_line.qty_received, 0) def test_correct_vendor_dropship(self): self.supplier_2 = self.env['res.partner'].create({'name': 'Vendor 2'}) # dropship route to be added in test self.dropship_product = self.env['product.product'].create({ 'name': "Pen drive", 'is_storable': "True", 'categ_id': self.env.ref('product.product_category_1').id, 'lst_price': 100.0, 'standard_price': 0.0, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'uom_po_id': self.env.ref('uom.product_uom_unit').id, 'seller_ids': [ (0, 0, { 'delay': 10, 'partner_id': self.supplier.id, 'min_qty': 2.0, 'price': 4 }), (0, 0, { 'delay': 5, 'partner_id': self.supplier_2.id, 'min_qty': 1.0, 'price': 10 }) ], }) self.env.user.groups_id += self.env.ref('stock.group_adv_location') so_form = Form(self.env['sale.order']) so_form.partner_id = self.customer with mute_logger('odoo.tests.common.onchange'): with so_form.order_line.new() as line: line.product_id = self.dropship_product line.product_uom_qty = 1 line.route_id = self.dropshipping_route sale_order_drp_shpng = so_form.save() sale_order_drp_shpng.action_confirm() purchase = self.env['purchase.order'].search([('partner_id', '=', self.supplier_2.id)]) self.assertTrue(purchase, "an RFQ should have been created by the scheduler") self.assertTrue((purchase.date_planned - purchase.date_order).days == 5, "The second supplier has a delay of 5 days") self.assertTrue(purchase.amount_untaxed == 10, "the suppliers sells the item for 10$") so_form = Form(self.env['sale.order']) so_form.partner_id = self.customer with mute_logger('odoo.tests.common.onchange'): with so_form.order_line.new() as line: line.product_id = self.dropship_product line.product_uom_qty = 2 line.route_id = self.dropshipping_route sale_order_drp_shpng = so_form.save() sale_order_drp_shpng.action_confirm() purchase = self.env['purchase.order'].search([('partner_id', '=', self.supplier.id)]) self.assertTrue(purchase, "an RFQ should have been created by the scheduler") self.assertTrue((purchase.date_planned - purchase.date_order).days == 10, "The first supplier has a delay of 10 days") self.assertTrue(purchase.amount_untaxed == 8, "The price should be 4 * 2") def test_add_dropship_product_to_subcontracted_service_po(self): """ P1, a service product subcontracted to vendor V P2, a dropshipped product provided by V Confirm a SO with 1 x P1. On the generated PO, add 1 x P2 and confirm. It should create a dropship picking. Process the picking. It should add one SOL for P2. """ supplier = self.dropship_product.seller_ids.partner_id delivery_addr = self.env['res.partner'].create({ 'name': 'Super Address', 'type': 'delivery', 'parent_id': self.customer.id, }) subcontracted_service = self.env['product.product'].create({ 'name': 'SuperService', 'type': 'service', 'service_to_purchase': True, 'seller_ids': [(0, 0, {'partner_id': supplier.id})], }) so = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'partner_shipping_id': delivery_addr.id, 'order_line': [(0, 0, { 'product_id': subcontracted_service.id, 'product_uom_qty': 1.00, })], }) so.action_confirm() po = so._get_purchase_orders() self.assertTrue(po) po.order_line = [(0, 0, { 'product_id': self.dropship_product.id, 'product_qty': 1.00, 'product_uom': self.dropship_product.uom_id.id, })] po.button_confirm() dropship = po.picking_ids self.assertTrue(dropship.is_dropship) self.assertRecordValues(dropship.move_ids, [ {'product_id': self.dropship_product.id, 'partner_id': delivery_addr.id}, ]) dropship.move_ids.quantity = 1 dropship.button_validate() self.assertEqual(dropship.state, 'done') self.assertRecordValues(so.order_line, [ {'product_id': subcontracted_service.id, 'product_uom_qty': 1.0, 'qty_delivered': 0.0}, {'product_id': self.dropship_product.id, 'product_uom_qty': 0.0, 'qty_delivered': 1.0}, ]) def test_dropship_lot_product_appears_in_stock_lot_report(self): dropship_product = self.lot_dropship_product sale_order = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [Command.create({ 'product_id': dropship_product.id, 'product_uom_qty': 2, })], }) sale_order.action_confirm() purchase_order = sale_order.procurement_group_id.purchase_line_ids.order_id purchase_order.button_confirm() dropship_picking = purchase_order.picking_ids dropship_picking.move_line_ids.lot_name = 'dropship product lot' dropship_picking.move_ids.picked = True dropship_picking.button_validate() for model in (self.env['sale.order'], self.env['stock.picking']): model.flush_model() customer_lots = self.env['stock.lot.report'].search([('partner_id', '=', self.customer.id)]) self.assertRecordValues( customer_lots, [{ 'lot_id': dropship_picking.move_line_ids.lot_id.id, 'picking_id': dropship_picking.id, 'quantity': 2.0, }] ) def test_delivery_type(self): # Create an operation type starting as incoming/internal. operation_type = self.env['stock.picking.type'].create({ "name": "test", "sequence_code": "TEST", "code": "incoming" }) # Update the code/type to outgoing/delivery. operation_type.write({ "code": "outgoing" }) # Trigger re-computes. operation_type.default_location_src_id operation_type.default_location_dest_id self.assertEqual(operation_type.code, "outgoing") # Expect source location to be warehouse's location. self.assertEqual( operation_type.default_location_src_id, operation_type.warehouse_id.lot_stock_id ) # Expect destination location to be customer's location. self.assertEqual( operation_type.default_location_dest_id, self.env.ref('stock.stock_location_customers') ) def test_dropship_return_backorders_bill_on_order(self): """ Dropshipped billed-on-order product Sell 10 Deliver 7 + backorder creation Return 2 Process the backorder (3) Re-return 2 Ensure all SVL values are correct """ product = self.dropship_product product.purchase_method = 'purchase' product.write({ 'seller_ids': [ Command.clear(), Command.create({ 'partner_id': self.supplier.id, 'min_qty': 1.0, 'price': 10 }), ], 'standard_price': 10, }) product.categ_id.property_cost_method = 'average' product.route_ids = self.dropshipping_route sale_order = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [Command.create({ 'product_id': product.id, 'product_uom_qty': 10.0, 'price_unit': 10, })] }) sale_order.action_confirm() purchase_order = sale_order.order_line.purchase_line_ids.order_id purchase_order.button_confirm() purchase_order.action_create_invoice() purchase_bill = purchase_order.invoice_ids purchase_bill.invoice_date = datetime.today() purchase_bill.action_post() dropship = sale_order.picking_ids dropship.move_ids.quantity = 7 res_dict = dropship.button_validate() backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(res_dict['context'])).save() backorder_wizard.process() backorder = dropship.backorder_ids return_form = Form(self.env['stock.return.picking'].with_context(active_id=dropship.id, active_model='stock.picking')) return_wizard = return_form.save() return_wizard.product_return_moves.quantity = 2 action = return_wizard.action_create_returns() return_picking = self.env['stock.picking'].browse(action['res_id']) return_picking.move_ids.quantity = 2 return_picking.button_validate() backorder.button_validate() return_form = Form(self.env['stock.return.picking'].with_context(active_id=return_picking.id, active_model='stock.picking')) return_wizard = return_form.save() return_wizard.product_return_moves.quantity = 2 action = return_wizard.action_create_returns() re_return = self.env['stock.picking'].browse(action['res_id']) re_return.move_ids.quantity = 2 re_return.button_validate() layers = sale_order.picking_ids.move_ids.stock_valuation_layer_ids.sorted('id') self.assertEqual(layers.mapped('value'), [ 70.0, -70.0, # Dropship 20.0, -20.0, # Return 30.0, -30.0, # Backorder 20.0, -20.0, # Re-return ]) @tagged('post_install', '-at_install') class TestDropshipPostInstall(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.supplier, cls.customer = cls.env['res.partner'].create([ {'name': 'Vendor Man'}, {'name': 'Customer Man'}, ]) cls.dropship_product = cls.env['product.product'].create({ 'name': 'Dropshipped Product', 'tracking': 'none', 'standard_price': 20, 'invoice_policy': 'delivery', 'seller_ids': [Command.create({ 'partner_id': cls.supplier.id, })], 'route_ids': [Command.link(cls.env.ref('stock_dropshipping.route_drop_shipping').id)], }) def test_dropshipping_tracked_product(self): supplier, customer = self.supplier, self.customer product_lot = self.dropship_product product_lot.categ_id.property_cost_method = 'standard' sale_order = self.env['sale.order'].create({ 'partner_id': customer.id, 'order_line': [Command.create({ 'product_id': product_lot.id, 'product_uom_qty': 1, })] }) sale_order.action_confirm() # Confirm PO purchase = self.env['purchase.order'].search([('partner_id', '=', supplier.id)]) self.assertTrue(purchase, "an RFQ should have been created") purchase.button_confirm() dropship_picking = sale_order.picking_ids dropship_picking.action_confirm() with Form(dropship_picking) as picking_form: with picking_form.move_ids_without_package.new() as move: move.product_id = product_lot move.quantity = 1 dropship_picking.button_validate() self.assertEqual(dropship_picking.state, 'done') def test_return_dropship_vendor_is_other_company(self): other_company = self.env['res.company'].create({'name': 'company vendor'}) product = self.dropship_product product.seller_ids.partner_id = other_company.partner_id.id sale_order = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [Command.create({ 'product_id': product.id, 'product_uom_qty': 2, })], }) sale_order.action_confirm() purchase_order = sale_order._get_purchase_orders() purchase_order.button_confirm() dropship_picking = purchase_order.picking_ids dropship_picking.move_ids.quantity = 2 dropship_picking.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 2) self.assertEqual(purchase_order.order_line.qty_received, 2) stock_return_picking_form = Form(self.env['stock.return.picking'].with_context( active_ids=dropship_picking.ids, active_id=dropship_picking.id, active_model='stock.picking', )) return_wiz = stock_return_picking_form.save() return_wiz.product_return_moves.quantity = 2 res = return_wiz.action_create_returns() return_picking = self.env['stock.picking'].browse(res['res_id']) return_picking.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 0) self.assertEqual(purchase_order.order_line.qty_received, 0)