Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] multi_warehouse_sale_order: enable warehouse selection in sale order line #639

Draft
wants to merge 2 commits into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions multi_warehouse_sale_order/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import tests
20 changes: 20 additions & 0 deletions multi_warehouse_sale_order/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': 'Multi Warehouse Sale Order',
'version': '1.0',
'category': 'Sales',
'summary': 'Enable multi-warehouse delivery',
'depends': ['sale_management', 'stock'],
'license': 'LGPL-3',
'author': 'Shiv Bhadaniya (sbha)',
'data': [
'views/product_template_views.xml',
'views/sale_order_line_views.xml',
'views/multi_warehouse_menus.xml',
],
'installable': True,
'assets': {
'web.assets_backend': [
'multi_warehouse_sale_order/static/src/**/*',
],
},
}
3 changes: 3 additions & 0 deletions multi_warehouse_sale_order/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import product_template
from . import sale_order_line
from . import sale_order
12 changes: 12 additions & 0 deletions multi_warehouse_sale_order/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import api, fields, models

class ProductTemplate(models.Model):
_inherit = 'product.template'

primary_warehouse_id = fields.Many2one('stock.warehouse', string='Primary Warehouse', required=True, default=lambda self: self._default_primary_warehouse())
secondary_warehouse_id = fields.Many2one('stock.warehouse', string='Secondary Warehouse')

@api.model
def _default_primary_warehouse(self):
"""Fetch the default primary warehouse (e.g., the first available one)."""
return self.env['stock.warehouse'].search([], limit=1).id
105 changes: 105 additions & 0 deletions multi_warehouse_sale_order/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from odoo import models
from odoo.exceptions import ValidationError

class SaleOrder(models.Model):
_inherit = 'sale.order'

# -------------------------------------------------------------------------
# METHODS
# -------------------------------------------------------------------------

def create_delivery_order(self, warehouse, order_lines):
"""Create a delivery order (picking) for the given warehouse and order lines."""
delivery_picking = self.env['stock.picking'].create({
'partner_id': self.partner_id.id,
'picking_type_id': warehouse.out_type_id.id,
'location_id': warehouse.lot_stock_id.id,
'location_dest_id': self.partner_id.property_stock_customer.id,
'origin': self.name,
})

for line in order_lines:
self.env['stock.move'].create({
'picking_id': delivery_picking.id,
'name': line.product_id.name,
'product_id': line.product_id.id,
'product_uom_qty': line.product_uom_qty,
'product_uom': line.product_uom.id,
'location_id': warehouse.lot_stock_id.id,
'location_dest_id': self.partner_id.property_stock_customer.id,
})

return delivery_picking

def check_stock_availability(self, product, warehouse, required_qty):
"""Check if a warehouse has sufficient stock for a given product."""
stock_quant = self.env['stock.quant'].search([
('product_id', '=', product.id),
('location_id', '=', warehouse.lot_stock_id.id)
], limit=1)
return stock_quant.quantity >= required_qty if stock_quant else False

def get_fulfillment_warehouse(self):
"""Determine the best warehouse strategy to minimize delivery orders."""

warehouse_product_count = {}
product_warehouse_map = {}

# Identify available warehouses for each product
for line in self.order_line:
product = line.product_id
primary_warehouse = product.primary_warehouse_id
secondary_warehouse = product.secondary_warehouse_id

available_warehouses = []
if self.check_stock_availability(product, primary_warehouse, line.product_uom_qty):
available_warehouses.append(primary_warehouse)
if secondary_warehouse and self.check_stock_availability(product, secondary_warehouse, line.product_uom_qty):
available_warehouses.append(secondary_warehouse)

if not available_warehouses:
raise ValidationError(f"Product '{product.name}' is out of stock in warehouses.")

# Count how many products each warehouse can fulfill
for warehouse in available_warehouses:
warehouse_product_count.setdefault(warehouse, 0)
warehouse_product_count[warehouse] += 1

product_warehouse_map[product] = available_warehouses

sorted_warehouses = sorted(warehouse_product_count, key=warehouse_product_count.get, reverse=True)

final_warehouse_assignment = {}

for product, available_warehouses in product_warehouse_map.items():
assigned_warehouse = None
for wh in sorted_warehouses:
if wh in available_warehouses:
assigned_warehouse = wh
break

if assigned_warehouse:
for line in self.order_line:
if line.product_id == product:
line.write({'warehouse_id': assigned_warehouse.id}) # Update SOL warehouse
if assigned_warehouse not in final_warehouse_assignment:
final_warehouse_assignment[assigned_warehouse] = []
final_warehouse_assignment[assigned_warehouse].append(line)
break

return final_warehouse_assignment

# -------------------------------------------------------------------------
# ACTIONS
# -------------------------------------------------------------------------

def action_confirm(self):
"""Confirm the sale order and create optimized delivery orders."""
warehouse_lines = self.get_fulfillment_warehouse()

for warehouse, lines in warehouse_lines.items():
res = self.create_delivery_order(warehouse, lines)
self.write({'picking_ids': [(4, res.id)]})

self.write({'state': 'sale'})
return True
80 changes: 80 additions & 0 deletions multi_warehouse_sale_order/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from odoo import api, fields, models
from odoo.exceptions import ValidationError

class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'

warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', readonly=False, domain="[('id', 'in', available_warehouse_ids)]")
available_warehouse_ids = fields.Many2many('stock.warehouse', compute='_compute_available_warehouses', store=True)
on_hand_qty_warehouse_wise = fields.Json(string="Available Stocks", compute="_compute_on_hand_qty_warehouse_wise", store=True)
forecast_qty_warehouse_wise = fields.Json(string="Stocks Forecast", compute="_compute_forecast_qty_warehouse_wise", store=True)

# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------

@api.depends('product_id')
def _compute_available_warehouses(self):
"""Compute available warehouses based on the product's primary and secondary warehouse"""

for line in self:
if line.product_id and not line.product_id.primary_warehouse_id:
raise ValidationError("Please add at least Primary warehouse")
warehouse_ids = []
if line.product_id.primary_warehouse_id:
product = line.product_id.product_tmpl_id
warehouse_ids.append(product.primary_warehouse_id.id)
if product.secondary_warehouse_id:
warehouse_ids.append(product.secondary_warehouse_id.id)
line.available_warehouse_ids = warehouse_ids

@api.depends('product_id')
def _compute_warehouse_id(self):
"""Set the primary warehouse for the product in the order line."""

for line in self:
if line.product_template_id and not line.warehouse_id:
line.warehouse_id = line.product_template_id.primary_warehouse_id

@api.depends('product_id')
def _compute_on_hand_qty_warehouse_wise(self):
"""Compute in-hand stock for each warehouse."""

all_warehouses = self.env['stock.warehouse'].search([])
for line in self:
if not line.product_id:
line.on_hand_qty_warehouse_wise = {}
continue

available_data = {}
for warehouse in all_warehouses:
stock_qty = sum(self.env['stock.quant'].search([
('product_id', '=', line.product_id.id),
('location_id', 'child_of', warehouse.lot_stock_id.id)
]).mapped('quantity'))
available_data[warehouse.code] = stock_qty

if not available_data:
available_data = {"No Stock": 0}

line.on_hand_qty_warehouse_wise = available_data

@api.depends('product_id')
def _compute_forecast_qty_warehouse_wise(self):
"""Compute forecasted stock for each warehouse."""

all_warehouses = self.env['stock.warehouse'].search([])
for line in self:
if not line.product_id:
line.forecast_qty_warehouse_wise = {}
continue

forecast_data = {}
for warehouse in all_warehouses:
res = self.env['stock.forecasted_product_product'].with_context(
warehouse_id=warehouse.id).get_report_values(docids=line.product_id.ids)
forecast_data[warehouse.code] = res['docs']['virtual_available']

if not forecast_data:
forecast_data = {"No Forecast": 0}
line.forecast_qty_warehouse_wise = forecast_data
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/** @odoo-module **/

import { patch } from "@web/core/utils/patch";
import {
QtyAtDateWidget,
qtyAtDateWidget
} from "@sale_stock/widgets/qty_at_date_widget";

/**
* Patch the QtyAtDateWidget to support warehouse-wise quantity data
*/
patch(QtyAtDateWidget.prototype, {
/**
* Update calculation data with warehouse-wise quantities when available
* @override
*/
updateCalcData() {
const { data } = this.props.record;

// Check if required data is available
if (!data.product_id || (!data.on_hand_qty_warehouse_wise && !data.forecast_qty_warehouse_wise)) {
return super.updateCalcData();
}

if (!data.on_hand_qty_warehouse_wise) {
return;
} else if (!data.forecast_qty_warehouse_wise) {
return;
}

// Set values from warehouse-wise data
this.calcData.available_qty = data.on_hand_qty_warehouse_wise || {};
this.calcData.forecast_qty = data.forecast_qty_warehouse_wise || {};

super.updateCalcData();
},
});

/**
* Extended stock widget with additional field dependencies
*/

patch(qtyAtDateWidget, {
fieldDependencies: [
{ name: "on_hand_qty_warehouse_wise", type: "object" },
{ name: "forecast_qty_warehouse_wise", type: "object" },
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<template id="multi_Warehouse_stock.QtyAtDateExtension" xml:space="preserve">

<t t-name="multi_Warehouse_stock.QtyAtDatePopover" t-inherit="sale_stock.QtyAtDatePopover" t-inherit-mode="extension">
<xpath expr="//div[@class='p-2']" position="attributes">
<attribute name="class">p-3 border rounded bg-white shadow</attribute>
</xpath>

<xpath expr="//h6" position="replace">
<h6 class="mb-2 text-muted">Availability</h6>
</xpath>

<!-- Available Stock section -->
<xpath expr="//tr[td/strong[text()='Available']]" position="replace">
<tr>
<td class="fw-bold">Available</td>
<td class="ps-4">
<div class="d-flex justify-content-between" t-foreach="Object.entries(props.calcData.available_qty)" t-as="warehouse" t-key="warehouse[0]">
<span class="fw-bold"><t t-out="warehouse[0]"/> - </span>
<t t-out="warehouse[1]"/> Units
</div>
</td>
</tr>
</xpath>

<!-- Forecasted Stock section -->
<xpath expr="//tr[td/strong[text()='Forecasted Stock']]" position="replace">
<tr>
<td><strong>Forecasted Stock</strong><br/><small>On <span t-out="props.calcData.delivery_date"/></small></td>

<td class="ps-4">
<div class="d-flex justify-content-between" t-foreach="Object.entries(props.calcData.forecast_qty)" t-as="warehouse" t-key="warehouse[0]">
<span class="fw-bold"><t t-out="warehouse[0]"/> - </span>
<t t-out="warehouse[1]"/> Units
</div>
</td>
</tr>
</xpath>
</t>

</template>
1 change: 1 addition & 0 deletions multi_warehouse_sale_order/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_multi_warehouse_sale_order
Loading