diff --git a/sale_line_change/__init__.py b/sale_line_change/__init__.py new file mode 100644 index 00000000..40272379 --- /dev/null +++ b/sale_line_change/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/sale_line_change/__manifest__.py b/sale_line_change/__manifest__.py new file mode 100644 index 00000000..f26c602a --- /dev/null +++ b/sale_line_change/__manifest__.py @@ -0,0 +1,25 @@ +{ + 'name': 'Sale Line Change', + 'summary': 'Change Confirmed Sale Lines Routes or Warehouses.', + 'version': '13.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Sale', + 'license': 'AGPL-3', + 'complexity': 'expert', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +""", + 'depends': [ + 'sale_sourced_by_line', + 'sale_stock', + 'stock_dropshipping', + ], + 'demo': [], + 'data': [ + 'wizard/sale_line_change_views.xml', + 'views/sale_views.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/sale_line_change/tests/__init__.py b/sale_line_change/tests/__init__.py new file mode 100644 index 00000000..27bc6136 --- /dev/null +++ b/sale_line_change/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_line_change diff --git a/sale_line_change/tests/test_sale_line_change.py b/sale_line_change/tests/test_sale_line_change.py new file mode 100644 index 00000000..129f9efc --- /dev/null +++ b/sale_line_change/tests/test_sale_line_change.py @@ -0,0 +1,99 @@ +from odoo.tests import common +from odoo.exceptions import ValidationError + + +class TestSaleLineChange(common.TransactionCase): + def setUp(self): + super(TestSaleLineChange, self).setUp() + self.warehouse0 = self.env.ref('stock.warehouse0') + self.warehouse1 = self.env['stock.warehouse'].create({ + 'company_id': self.env.user.company_id.id, + # 'partner_id': self.env.user.company_id.partner_id.id, + 'name': 'TWH1', + 'code': 'TWH1', + }) + self.product1 = self.env.ref('product.product_product_24') + self.partner1 = self.env.ref('base.res_partner_12') + self.so1 = self.env['sale.order'].create({ + 'partner_id': self.partner1.id, + 'partner_invoice_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'order_line': [(0, 0, { + 'product_id': self.product1.id, + 'name': 'N/A', + 'product_uom_qty': 1.0, + 'price_unit': 100.0, + })] + }) + self.dropship_route = self.env.ref('stock_dropshipping.route_drop_shipping') + self.warehouse0_route = self.warehouse0.route_ids.filtered(lambda r: r.name.find('Deliver') >= 0) + + def test_00_sale_change_warehouse(self): + so = self.so1 + + so.action_confirm() + self.assertTrue(so.state in ('sale', 'done')) + self.assertTrue(so.picking_ids) + org_picking = so.picking_ids + self.assertEqual(org_picking.picking_type_id.warehouse_id, self.warehouse0) + + wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({}) + self.assertTrue(wiz.line_ids) + wiz.line_ids.line_warehouse_id = self.warehouse1 + wiz.line_ids.line_date_planned = '2018-01-01 00:00:00' + wiz.apply() + + self.assertTrue(len(so.picking_ids) == 2) + self.assertTrue(org_picking.state == 'cancel') + new_picking = so.picking_ids - org_picking + self.assertTrue(new_picking) + self.assertEqual(new_picking.picking_type_id.warehouse_id, self.warehouse1) + self.assertEqual(str(new_picking.scheduled_date), '2018-01-01 00:00:00') + + def test_01_sale_change_route(self): + so = self.so1 + + so.action_confirm() + self.assertTrue(so.state in ('sale', 'done')) + self.assertTrue(so.picking_ids) + org_picking = so.picking_ids + self.assertEqual(org_picking.picking_type_id.warehouse_id, self.warehouse0) + + # Change route on wizard line + wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({}) + self.assertTrue(wiz.line_ids) + wiz.line_ids.line_route_id = self.dropship_route + wiz.apply() + + # Check that RFQ/PO was created. + self.assertTrue(org_picking.state == 'cancel') + po_line = self.env['purchase.order.line'].search([('sale_line_id', '=', so.order_line.id)]) + self.assertTrue(po_line) + + def test_02_sale_dropshipping_to_warehouse(self): + self.assertTrue(self.warehouse0_route) + self.product1.route_ids += self.dropship_route + so = self.so1 + + so.action_confirm() + self.assertTrue(so.state in ('sale', 'done')) + self.assertFalse(so.picking_ids) + + # Change route on wizard line + wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({}) + self.assertTrue(wiz.line_ids) + wiz.line_ids.line_route_id = self.warehouse0_route + wiz.line_ids.line_date_planned = '2018-01-01 00:00:00' + + # Wizard cannot complete because of non-cancelled Purchase Order. + with self.assertRaises(ValidationError): + wiz.apply() + + po_line = self.env['purchase.order.line'].search([('sale_line_id', '=', so.order_line.id)]) + po_line.order_id.button_cancel() + wiz.apply() + + # Check parameters on new picking + self.assertTrue(so.picking_ids) + self.assertEqual(so.picking_ids.picking_type_id.warehouse_id, self.warehouse0) + self.assertEqual(str(so.picking_ids.scheduled_date), '2018-01-01 00:00:00') diff --git a/sale_line_change/views/sale_views.xml b/sale_line_change/views/sale_views.xml new file mode 100644 index 00000000..57dafe2c --- /dev/null +++ b/sale_line_change/views/sale_views.xml @@ -0,0 +1,19 @@ + + + + sale.order.form.inherit + sale.order + + + + + + + + \ No newline at end of file diff --git a/sale_line_change/wizard/__init__.py b/sale_line_change/wizard/__init__.py new file mode 100644 index 00000000..1307b5ec --- /dev/null +++ b/sale_line_change/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_line_change diff --git a/sale_line_change/wizard/sale_line_change.py b/sale_line_change/wizard/sale_line_change.py new file mode 100644 index 00000000..d7ad4005 --- /dev/null +++ b/sale_line_change/wizard/sale_line_change.py @@ -0,0 +1,88 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class SaleLineChangeOrder(models.TransientModel): + _name = 'sale.line.change.order' + _description = 'Sale Line Change Order' + + order_id = fields.Many2one('sale.order', string='Sale Order') + line_ids = fields.One2many('sale.line.change.order.line', 'change_order_id', string='Change Lines') + + @api.model + def default_get(self, fields): + rec = super(SaleLineChangeOrder, self).default_get(fields) + if 'order_id' in rec: + order = self.env['sale.order'].browse(rec['order_id']) + if not order: + return rec + + line_model = self.env['sale.line.change.order.line'] + rec['line_ids'] = [(0, 0, line_model.values_from_so_line(l)) for l in order.order_line] + return rec + + def apply(self): + self.ensure_one() + self.line_ids.apply() + return True + + +class SaleLineChangeOrderLine(models.TransientModel): + _name = 'sale.line.change.order.line' + + change_order_id = fields.Many2one('sale.line.change.order') + sale_line_id = fields.Many2one('sale.order.line', string='Sale Line') + line_ordered_qty = fields.Float(string='Ordered Qty') + line_delivered_qty = fields.Float(string='Delivered Qty') + line_reserved_qty = fields.Float(string='Reserved Qty') + line_date_planned = fields.Datetime(string='Planned Date') + line_warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') + line_route_id = fields.Many2one('stock.location.route', string='Route') + + def values_from_so_line(self, so_line): + move_ids = so_line.move_ids + reserved_qty = sum(move_ids.mapped('reserved_availability')) + return { + 'sale_line_id': so_line.id, + 'line_ordered_qty': so_line.product_uom_qty, + 'line_delivered_qty': so_line.qty_delivered, + 'line_reserved_qty': reserved_qty, + 'line_date_planned': so_line.date_planned, + 'line_warehouse_id': so_line.warehouse_id.id, + 'line_route_id': so_line.route_id.id, + } + + def _apply(self): + self._apply_clean_dropship() + self._apply_clean_existing_moves() + self._apply_new_values() + self._apply_procurement() + + def _apply_clean_dropship(self): + po_line_model = self.env['purchase.order.line'].sudo() + po_lines = po_line_model.search([('sale_line_id', 'in', self.mapped('sale_line_id.id'))]) + + if po_lines and po_lines.filtered(lambda l: l.order_id.state != 'cancel'): + names = ', '.join(po_lines.filtered(lambda l: l.order_id.state != 'cancel').mapped('order_id.name')) + raise ValidationError('One or more lines has existing non-cancelled Purchase Orders associated: ' + names) + + def _apply_clean_existing_moves(self): + moves = self.mapped('sale_line_id.move_ids').filtered(lambda m: m.state != 'done') + moves._action_cancel() + + def _apply_new_values(self): + for line in self: + line.sale_line_id.write({ + 'date_planned': line.line_date_planned, + 'warehouse_id': line.line_warehouse_id.id, + 'route_id': line.line_route_id.id, + }) + + def _apply_procurement(self): + self.mapped('sale_line_id')._action_launch_stock_rule() + + def apply(self): + changed_lines = self.filtered(lambda l: ( + l.sale_line_id.warehouse_id != l.line_warehouse_id + or l.sale_line_id.route_id != l.line_route_id)) + changed_lines._apply() diff --git a/sale_line_change/wizard/sale_line_change_views.xml b/sale_line_change/wizard/sale_line_change_views.xml new file mode 100644 index 00000000..bbdb6976 --- /dev/null +++ b/sale_line_change/wizard/sale_line_change_views.xml @@ -0,0 +1,40 @@ + + + + sale.line.change.order.form + sale.line.change.order + form + + + Changing Date Planned alone should be done on any existing Pickings or POs. + + + + + + + + + + + + + + + + + + Sale Line Change Order + sale.line.change.order + + form + + new + + \ No newline at end of file
Changing Date Planned alone should be done on any existing Pickings or POs.