Merge branch 'mig/13.0/sale_sourced_by_line' into '13.0'

mig/13.0/sale_sourced_by_line into 13.0

See merge request hibou-io/hibou-odoo/suite!562
This commit is contained in:
Jared Kipe
2020-10-31 15:51:48 +00:00
8 changed files with 233 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
============================
Hibou - Sale Sourced by Line
============================
Adds warehouse and planned date fields to sale order lines. Will split the delivery orders
to every distinct warehouse.
Additionally, adds fields per line and to the sale order to set the planned date on generated
delivery orders.
=======
License
=======
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/master/LICENSE>`_.
Copyright Hibou Corp. 2018

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,33 @@
{
'name': 'Sale Sourced by Line',
'summary': 'Multiple warehouse source locations for Sale order',
'version': '13.0.1.0.0',
'author': "Hibou Corp.,Odoo Community Association (OCA)",
'category': 'Warehouse',
'license': 'AGPL-3',
'complexity': 'expert',
'images': [],
'website': "https://hibou.io",
'description': """
Sale Sourced by Line
====================
Adds the possibility to source a line of sale order from a specific
warehouse instead of using the warehouse of the sale order. Additionally,
adds the ability to set the planned date of the outgoing shipments on a
per order or per order line basis.
This module was inspired by a module by the same name from the OCA for 9.0,
however it does not necessarily work in the same ways or have the same features.
""",
'depends': [
'sale_stock',
],
'demo': [],
'data': [
'views/sale_views.xml',
],
'auto_install': False,
'installable': True,
}

View File

@@ -0,0 +1 @@
from . import sale

View File

@@ -0,0 +1,84 @@
from collections import defaultdict
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
date_planned = fields.Datetime('Planned Date')
requested_date = fields.Datetime('Requested Date')
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
# In 13, this field exists, but isn't stored and is computed during
# computation for available inventory (set to order's warehouse)
warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse',
compute=None, store=True)
date_planned = fields.Datetime('Planned Date')
def _prepare_procurement_values(self, group_id=False):
vals = super(SaleOrderLine, self)._prepare_procurement_values(group_id=group_id)
if self.warehouse_id:
vals.update({'warehouse_id': self.warehouse_id})
if self.date_planned:
vals.update({'date_planned': self.date_planned})
elif self.order_id.date_planned:
vals.update({'date_planned': self.order_id.date_planned})
return vals
# Needs modifications to not actually set a warehouse on the line as it is now stored
@api.depends('product_id', 'customer_lead', 'product_uom_qty', 'product_uom', 'order_id.warehouse_id', 'order_id.commitment_date')
def _compute_qty_at_date(self):
""" Compute the quantity forecasted of product at delivery date. There are
two cases:
1. The quotation has a commitment_date, we take it as delivery date
2. The quotation hasn't commitment_date, we compute the estimated delivery
date based on lead time"""
qty_processed_per_product = defaultdict(lambda: 0)
grouped_lines = defaultdict(lambda: self.env['sale.order.line'])
# We first loop over the SO lines to group them by warehouse and schedule
# date in order to batch the read of the quantities computed field.
for line in self:
if not (line.product_id and line.display_qty_widget):
continue
# use warehouse from line or order
warehouse = line.warehouse_id or line.order_id.warehouse_id
# line.warehouse_id = line.order_id.warehouse_id
if line.order_id.commitment_date:
date = line.order_id.commitment_date
else:
date = line._expected_date()
grouped_lines[(warehouse, date)] |= line
treated = self.browse()
for (warehouse, scheduled_date), lines in grouped_lines.items():
product_qties = lines.mapped('product_id').with_context(to_date=scheduled_date, warehouse=warehouse).read([
'qty_available',
'free_qty',
'virtual_available',
])
qties_per_product = {
product['id']: (product['qty_available'], product['free_qty'], product['virtual_available'])
for product in product_qties
}
for line in lines:
line.scheduled_date = scheduled_date
qty_available_today, free_qty_today, virtual_available_at_date = qties_per_product[line.product_id.id]
line.qty_available_today = qty_available_today - qty_processed_per_product[line.product_id.id]
line.free_qty_today = free_qty_today - qty_processed_per_product[line.product_id.id]
line.virtual_available_at_date = virtual_available_at_date - qty_processed_per_product[line.product_id.id]
if line.product_uom and line.product_id.uom_id and line.product_uom != line.product_id.uom_id:
line.qty_available_today = line.product_id.uom_id._compute_quantity(line.qty_available_today, line.product_uom)
line.free_qty_today = line.product_id.uom_id._compute_quantity(line.free_qty_today, line.product_uom)
line.virtual_available_at_date = line.product_id.uom_id._compute_quantity(line.virtual_available_at_date, line.product_uom)
qty_processed_per_product[line.product_id.id] += line.product_uom_qty
treated |= lines
remaining = (self - treated)
remaining.virtual_available_at_date = False
remaining.scheduled_date = False
remaining.free_qty_today = False
remaining.qty_available_today = False
# don't unset warehouse as it may be set by hand
# remaining.warehouse_id = False

View File

@@ -0,0 +1 @@
from . import test_sale_sources

View File

@@ -0,0 +1,63 @@
from odoo.tests import common
class TestSaleSources(common.TransactionCase):
def setUp(self):
super(TestSaleSources, self).setUp()
self.partner = self.env.ref('base.res_partner_2')
self.product_1 = self.env['product.product'].create({
'type': 'consu',
'name': 'Test Product 1',
})
self.product_2 = self.env['product.product'].create({
'type': 'consu',
'name': 'Test Product 2',
})
self.wh_1 = self.env.ref('stock.warehouse0')
self.wh_2 = self.env['stock.warehouse'].create({
'name': 'Test WH2',
'code': 'TWH2',
})
def test_plan_one_warehouse(self):
so = self.env['sale.order'].create({
'warehouse_id': self.wh_1.id,
'partner_id': self.partner.id,
'date_planned': '2018-01-01',
'order_line': [(0, 0, {
'product_id': self.product_1.id,
'product_uom_qty': 1.0,
'product_uom': self.product_1.uom_id.id,
'price_unit': 10.0,
}),
(0, 0, {
'product_id': self.product_2.id,
'product_uom_qty': 1.0,
'product_uom': self.product_2.uom_id.id,
'price_unit': 10.0,
})]
})
so.action_confirm()
self.assertTrue(so.state in ('sale', 'done'))
self.assertEqual(len(so.picking_ids), 1)
self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_1)), 1)
self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_2)), 0)
def test_plan_two_warehouses(self):
so = self.env['sale.order'].create({
'warehouse_id': self.wh_1.id,
'partner_id': self.partner.id,
'date_planned': '2018-01-01',
'order_line': [(0, 0, {'product_id': self.product_1.id}),
(0, 0, {'product_id': self.product_2.id,
'date_planned': '2018-02-01', 'warehouse_id': self.wh_2.id})]
})
# in 13 default computation, this would result in a failure
self.assertTrue(so.order_line.filtered(lambda l: l.warehouse_id))
so.action_confirm()
self.assertTrue(so.state in ('sale', 'done'))
self.assertEqual(len(so.picking_ids), 2)
self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_1)), 1)
self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_2)), 1)

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_order_form" model="ir.ui.view">
<field name="name">sale.order.form.warehouse</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='route_id']" position="before">
<field name="date_planned"/>
<field name="warehouse_id"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='route_id']" position="before">
<field name="date_planned"/>
<field name="warehouse_id"/>
</xpath>
<xpath expr="//field[@name='commitment_date']" position="before">
<field name="date_planned" />
<field name="requested_date" />
</xpath>
</field>
</record>
<record id="view_order_line_tree" model="ir.ui.view">
<field name="name">sale.order.line.tree.warehouse</field>
<field name="model">sale.order.line</field>
<field name="inherit_id" ref="sale.view_order_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='route_id']" position="before">
<field name="date_planned"/>
<field name="warehouse_id"/>
</xpath>
</field>
</record>
</odoo>