mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
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:
17
sale_sourced_by_line/README.rst
Normal file
17
sale_sourced_by_line/README.rst
Normal 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
|
||||||
1
sale_sourced_by_line/__init__.py
Normal file
1
sale_sourced_by_line/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
33
sale_sourced_by_line/__manifest__.py
Normal file
33
sale_sourced_by_line/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
1
sale_sourced_by_line/models/__init__.py
Normal file
1
sale_sourced_by_line/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import sale
|
||||||
84
sale_sourced_by_line/models/sale.py
Normal file
84
sale_sourced_by_line/models/sale.py
Normal 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
|
||||||
1
sale_sourced_by_line/tests/__init__.py
Normal file
1
sale_sourced_by_line/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_sale_sources
|
||||||
63
sale_sourced_by_line/tests/test_sale_sources.py
Normal file
63
sale_sourced_by_line/tests/test_sale_sources.py
Normal 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)
|
||||||
33
sale_sourced_by_line/views/sale_views.xml
Normal file
33
sale_sourced_by_line/views/sale_views.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user