commit 97ae58fb55a542d86160f152e15cffc74b4a01cb Author: Suherdy Yacob Date: Sat Oct 4 11:33:39 2025 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..6babc6c --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Subcontracting Inventory Move First + +## Overview +This module extends the standard Odoo subcontracting workflow to allow creating inventory moves first and then linking them to purchase orders, rather than the standard flow where purchase orders create inventory moves. + +## Features +- Create "Resupply Subcontract" inventory moves directly in the inventory module +- Link these moves to purchase orders after creation +- Smart button on purchase orders to view linked inventory moves +- Maintain data consistency between moves and purchase orders +- Update source document field in inventory moves and related stock pickings to show purchase order number +- Proper handling of purchase order cancellation to unlink moves + +## Usage + +### Creating Subcontracting Inventory Moves First +1. Navigate to Inventory > Operations > Transfers +2. Create a new transfer with operation type "Resupply Subcontractor" +3. The inventory move will be created without being linked to a purchase order initially + +### Linking to Purchase Orders +1. Go to the purchase order you want to link +2. Click the "Link Subcontracting Moves" button +3. Select the appropriate inventory moves from the list +4. Click the "Link to Purchase Order" button at the top of the list view to confirm linking +5. The moves will be linked to the purchase order and source document fields updated + +### Viewing Linked Moves +1. On any purchase order form, the smart button "Subcontracting Moves" will show the count of linked moves +2. Click the button to view and manage all linked inventory moves + +## Technical Details +- The module adds a `purchase_order_id` field to `stock.move` to track links to purchase orders +- The module adds a `subcontracting_move_ids` field to `purchase.order` to track linked moves +- Business logic ensures data consistency when linking/unlinking moves and orders +- When linking moves to purchase orders, the source document field is updated to show the purchase order number +- When linking moves to purchase orders, the source document field in related stock pickings is also updated +- Proper handling of purchase order cancellation to unlink moves without errors + +## Compatibility +- Compatible with Odoo 18.0 +- Works alongside standard subcontracting workflow +- Depends on: mrp_subcontracting, purchase, stock, mrp_subcontracting_purchase \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..2a154d8 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'Subcontracting Inventory Move First', + 'version': '18.0.1.0.0', + 'category': 'Manufacturing/Manufacturing', + 'summary': 'Allow creating inventory moves first and then linking to purchase orders in subcontracting', + 'author' : "Suherdy Yacob", + 'description': """ + This module extends the standard subcontracting workflow to allow creating + inventory moves first and then linking them to purchase orders, + rather than the standard flow where purchase orders create inventory moves. + """, + 'depends': [ + 'mrp_subcontracting', + 'purchase', + 'stock', + 'mrp_subcontracting_purchase', + ], + 'data': [ + 'views/stock_move_views.xml', + 'views/stock_move_link_views.xml', + 'views/purchase_order_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d53d160 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..a215966 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_move +from . import purchase_order \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..520c2d8 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/purchase_order.cpython-312.pyc b/models/__pycache__/purchase_order.cpython-312.pyc new file mode 100644 index 0000000..bd5947c Binary files /dev/null and b/models/__pycache__/purchase_order.cpython-312.pyc differ diff --git a/models/__pycache__/stock_move.cpython-312.pyc b/models/__pycache__/stock_move.cpython-312.pyc new file mode 100644 index 0000000..9fe7bdc Binary files /dev/null and b/models/__pycache__/stock_move.cpython-312.pyc differ diff --git a/models/purchase_order.py b/models/purchase_order.py new file mode 100644 index 0000000..c02f110 --- /dev/null +++ b/models/purchase_order.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + # Field to link purchase order to inventory moves + subcontracting_move_ids = fields.Many2many( + 'stock.move', + string='Subcontracting Inventory Moves', + relation='purchase_order_stock_move_subcontract_rel', + column1='purchase_order_id', + column2='stock_move_id', + help='Inventory moves linked to this purchase order for subcontracting' + ) + + # Computed field to show count of linked inventory moves for smart button + subcontracting_move_count = fields.Integer( + string='Subcontracting Moves Count', + compute='_compute_subcontracting_move_count' + ) + + @api.depends('subcontracting_move_ids') + def _compute_subcontracting_move_count(self): + for order in self: + order.subcontracting_move_count = len(order.subcontracting_move_ids) + + def action_link_subcontracting_moves(self): + """Action to link existing inventory moves to this purchase order""" + self.ensure_one() + return { + 'name': 'Link Subcontracting Inventory Moves', + 'type': 'ir.actions.act_window', + 'view_mode': 'list', + 'res_model': 'stock.move', + 'domain': [ + ('location_dest_id.name', '=', 'Subcontracting Location'), + ('purchase_order_id', '=', False), + ('state', 'in', ['done']), + ], + 'target': 'new', + 'context': { + 'default_purchase_order_id': self.id, + }, + 'views': [ + (self.env.ref('subcontracting_inventory_move_first.view_stock_move_link_subcontracting_tree').id, 'list') + ], + 'limit': 1, + } + + def action_view_subcontracting_moves(self): + """Action to view linked subcontracting inventory moves""" + self.ensure_one() + action = { + 'name': 'Subcontracting Inventory Moves', + 'type': 'ir.actions.act_window', + 'view_mode': 'list,form', + 'res_model': 'stock.move', + 'domain': [('id', 'in', self.subcontracting_move_ids.ids)], + 'context': {'create': False} + } + if len(self.subcontracting_move_ids) == 1: + action['views'] = [(False, 'form')] + action['res_id'] = self.subcontracting_move_ids.id + return action + + def link_selected_subcontracting_move(self, move_id): + """Method to link a selected subcontracting move to this purchase order""" + self.ensure_one() + move = self.env['stock.move'].browse(move_id) + if move and move.exists() and not move.purchase_order_id: + # Link the move to this purchase order + move.write({ + 'purchase_order_id': self.id, + 'origin': self.name # Update source document to purchase order number + }) + + # Also update the related stock.picking if it exists + if move.picking_id: + move.picking_id.write({ + 'origin': self.name, + # Note: stock.picking doesn't have a purchase_order_id field, so we can't link it directly + }) + + # Also add to the Many2many field + current_moves = self.subcontracting_move_ids.ids + current_moves.append(move.id) + self.write({'subcontracting_move_ids': [(6, 0, current_moves)]}) + return True + return False + + @api.model + def create(self, vals): + """Override create to handle linking with inventory moves if provided""" + # Extract subcontracting moves from vals if they exist + subcontracting_move_ids = vals.get('subcontracting_move_ids') + + # Create the purchase order first + purchase_order = super().create(vals) + + # If subcontracting moves were provided, link them + if subcontracting_move_ids: + # Process the commands in the Many2many field + for command in subcontracting_move_ids: + if command[0] == 6: # Replace all + move_ids = command[2] + moves = self.env['stock.move'].browse(move_ids) + moves.write({'purchase_order_id': purchase_order.id}) + elif command[0] == 4: # Add one + move_id = command[1] + move = self.env['stock.move'].browse(move_id) + move.write({'purchase_order_id': purchase_order.id}) + + return purchase_order + + def write(self, vals): + """Override write to handle linking/unlinking of inventory moves""" + # Handle subcontracting moves linking/unlinking + if 'subcontracting_move_ids' in vals: + # Get current linked moves before the update + old_moves = self.subcontracting_move_ids + + # Call super to update the record + result = super().write(vals) + + # Update the purchase_order_id on the linked moves + for order in self: + # Unlink old moves that are no longer linked + moves_to_unlink = old_moves - order.subcontracting_move_ids + moves_to_unlink.write({'purchase_order_id': False}) + + # Link new moves + moves_to_link = order.subcontracting_move_ids - old_moves + moves_to_link.write({'purchase_order_id': order.id}) + else: + result = super().write(vals) + + return result + + def button_confirm(self): + """Override to handle subcontracting moves when confirming purchase order""" + # First call the original confirm method + result = super().button_confirm() + + # For each linked subcontracting move, potentially trigger additional actions + for order in self: + for move in order.subcontracting_move_ids: + # When the purchase order is confirmed, we may want to update the move's state + # or trigger the standard subcontracting flow for these moves + if move.state in ['draft', 'waiting']: + # Confirm the linked moves if they're not already confirmed + if move.state == 'draft': + move._action_confirm() + # Assign them if needed + if move.state in ['confirmed', 'waiting']: + move._action_assign() + + return result + + def button_cancel(self): + """Override to handle subcontracting moves when cancelling purchase order""" + # For each linked subcontracting move, we may need to unlink or update them + for order in self: + # Unlink moves if needed based on business requirements + # Here we'll keep the moves but remove the purchase order link + order.subcontracting_move_ids.write({'purchase_order_id': False}) + + # Then call the original cancel method + return super().button_cancel() \ No newline at end of file diff --git a/models/stock_move.py b/models/stock_move.py new file mode 100644 index 0000000..9f717a8 --- /dev/null +++ b/models/stock_move.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + + +class StockMove(models.Model): + _inherit = 'stock.move' + + # Field to link inventory move to a purchase order + purchase_order_id = fields.Many2one( + 'purchase.order', + string='Linked Purchase Order', + ondelete='set null', + help='Purchase order linked to this subcontracting inventory move' + ) + + # Computed field to show count of linked purchase orders for smart button + linked_purchase_order_count = fields.Integer( + string='Linked Purchase Orders Count', + compute='_compute_linked_purchase_order_count' + ) + + @api.depends('purchase_order_id') + def _compute_linked_purchase_order_count(self): + for move in self: + move.linked_purchase_order_count = 1 if move.purchase_order_id else 0 + + def action_view_linked_purchase_order(self): + """Action to view the linked purchase order from the inventory move""" + self.ensure_one() + if self.purchase_order_id: + return { + 'name': 'Linked Purchase Order', + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'purchase.order', + 'res_id': self.purchase_order_id.id, + 'target': 'current', + } + return { + 'name': 'Create Purchase Order', + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'purchase.order', + 'context': { + 'default_order_line': [(0, 0, { + 'product_id': self.product_id.id, + 'product_qty': self.product_uom_qty, + 'product_uom': self.product_uom.id, + })], + }, + 'target': 'current', + } + + def _action_confirm(self, merge=True, merge_into=False): + """Override to handle our custom subcontracting flow""" + # Call the super method first to maintain standard functionality + result = super()._action_confirm(merge=merge, merge_into=merge_into) + + # For moves that are subcontracting related but not linked to purchase orders yet + # we want to allow them to exist in the system without creating manufacturing orders immediately + for move in self: + if move.is_subcontract and not move.purchase_order_id: + # This is a move created first without a purchase order - allow it to exist + # We don't want to trigger the standard subcontracting flow here + # Prevent creating MOs for moves that are not linked to a purchase order yet + continue + + return result + + def _action_assign(self): + """Override to handle custom subcontracting flow""" + # For moves that are subcontracting but not linked to purchase orders yet, + # we may want to handle them differently + subcontract_moves_without_po = self.filtered(lambda m: m.is_subcontract and not m.purchase_order_id) + other_moves = self - subcontract_moves_without_po + + # Process other moves normally + if other_moves: + super(StockMove, other_moves)._action_assign() + + # For subcontract moves without PO, we can decide how to handle them + # For now, just call the super method + if subcontract_moves_without_po: + super(StockMove, subcontract_moves_without_po)._action_assign() + + return True + + def write(self, values): + """Override write to handle purchase order linking""" + # If we're linking a purchase order to a move, update the reverse link + if 'purchase_order_id' in values: + purchase_order = self.env['purchase.order'].browse(values['purchase_order_id']) + if purchase_order.exists(): + # Add this move to the purchase order's subcontracting moves + for move in self: + if move.is_subcontract and move not in purchase_order.subcontracting_move_ids: + # Add to the existing list of moves + current_moves = purchase_order.subcontracting_move_ids.ids + current_moves.append(move.id) + purchase_order.write({'subcontracting_move_ids': [(6, 0, current_moves)]}) + + return super().write(values) \ No newline at end of file diff --git a/views/purchase_order_views.xml b/views/purchase_order_views.xml new file mode 100644 index 0000000..db82482 --- /dev/null +++ b/views/purchase_order_views.xml @@ -0,0 +1,76 @@ + + + + + purchase.order.form.inherit.subcontracting.inventory.move.first + purchase.order + + + + + + + + + + + + + + + + + + + + + + Subcontracting Inventory Moves + ir.actions.act_window + stock.move + list,form + [('location_dest_id.name', '=', 'Subcontracting Location')] + {'search_default_location_dest_id_name': 'Subcontracting Location'} + + + + + + + + Link to Purchase Order + + + code + +if records: + # Get the active purchase order from context + po_id = env.context.get('default_purchase_order_id') or env.context.get('active_id') + if po_id: + po = env['purchase.order'].browse(po_id) + for record in records: + po.link_selected_subcontracting_move(record.id) +# Close the wizard window +action = {'type': 'ir.actions.act_window_close'} + + + \ No newline at end of file diff --git a/views/stock_move_link_views.xml b/views/stock_move_link_views.xml new file mode 100644 index 0000000..459f1ec --- /dev/null +++ b/views/stock_move_link_views.xml @@ -0,0 +1,26 @@ + + + + + stock.move.link.subcontracting.tree + stock.move + + +
+
+ + + + + + + +
+
+
+
\ No newline at end of file diff --git a/views/stock_move_views.xml b/views/stock_move_views.xml new file mode 100644 index 0000000..33d7d7a --- /dev/null +++ b/views/stock_move_views.xml @@ -0,0 +1,43 @@ + + + + + stock.move.form.inherit.subcontracting.inventory.move.first + stock.move + + + + + + + + + + + + + + stock.move.tree.inherit.subcontracting.inventory.move.first + stock.move + + + + + + + + + + + View Linked Purchase Order + + + code + action = record.action_view_linked_purchase_order() + +