[IMP] (purchase_)stock: suppliers information in replenishment
Infobox to help the user with the replenishment. Allow those actions: - See the different supplier with their lead time, price and quantity - Be able to select a supplier and set it on the orderpoint ⁻ See the last delivery by mmonth for the product - Last purchase date for each supplier Technicaly it use a wizard and a fields char with JSON used by a widget to display a static template. Also it creates directly the wizard in backend instead of just let the view manage the new object since we need a button on a one2many and if the records do not have an id, it's not possilbe to use it. closes odoo/odoo#72039 Task: 2519761 Related: odoo/upgrade#2596 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
@@ -111,11 +111,11 @@ class StockRule(models.Model):
|
||||
date_planned = date_planned - relativedelta(hours=1)
|
||||
return date_planned
|
||||
|
||||
def _get_lead_days(self, product):
|
||||
def _get_lead_days(self, product, **values):
|
||||
"""Add the product and company manufacture delay to the cumulative delay
|
||||
and cumulative description.
|
||||
"""
|
||||
delay, delay_description = super()._get_lead_days(product)
|
||||
delay, delay_description = super()._get_lead_days(product, **values)
|
||||
bypass_delay_description = self.env.context.get('bypass_delay_description')
|
||||
manufacture_rule = self.filtered(lambda r: r.action == 'manufacture')
|
||||
if not manufacture_rule:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from . import models
|
||||
from . import report
|
||||
from . import populate
|
||||
from . import wizard
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
'report/purchase_report_templates.xml',
|
||||
'report/report_stock_forecasted.xml',
|
||||
'report/report_stock_rule.xml',
|
||||
'wizard/stock_replenishment_info.xml'
|
||||
],
|
||||
'demo': [
|
||||
'data/purchase_stock_demo.xml',
|
||||
|
||||
@@ -69,3 +69,49 @@ class ProductProduct(models.Model):
|
||||
qty_by_product_location[(product.id, location.id)] += product_qty
|
||||
qty_by_product_wh[(product.id, location.warehouse_id.id)] += product_qty
|
||||
return qty_by_product_location, qty_by_product_wh
|
||||
|
||||
|
||||
class SupplierInfo(models.Model):
|
||||
_inherit = 'product.supplierinfo'
|
||||
|
||||
last_purchase_date = fields.Date('Last Purchase', compute='_compute_last_purchase_date')
|
||||
show_set_supplier_button = fields.Boolean(
|
||||
'Show Set Supplier Button', compute='_compute_show_set_supplier_button')
|
||||
|
||||
def _compute_last_purchase_date(self):
|
||||
self.last_purchase_date = False
|
||||
purchases = self.env['purchase.order'].search([
|
||||
('state', 'in', ('purchase', 'done')),
|
||||
('order_line.product_id', 'in',
|
||||
self.product_tmpl_id.product_variant_ids.ids),
|
||||
('partner_id', 'in', self.name.ids),
|
||||
], order='date_order')
|
||||
for supplier in self:
|
||||
products = supplier.product_tmpl_id.product_variant_ids
|
||||
for purchase in purchases:
|
||||
if purchase.partner_id != supplier.name:
|
||||
continue
|
||||
if not (products & purchase.order_line.product_id):
|
||||
continue
|
||||
supplier.last_purchase_date = purchase.date_order
|
||||
break
|
||||
|
||||
def _compute_show_set_supplier_button(self):
|
||||
self.show_set_supplier_button = True
|
||||
orderpoint_id = self.env.context.get('default_orderpoint_id')
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
if orderpoint_id:
|
||||
self.filtered(
|
||||
lambda s: s.id == orderpoint.supplier_id.id
|
||||
).show_set_supplier_button = False
|
||||
|
||||
def action_set_supplier(self):
|
||||
self.ensure_one()
|
||||
orderpoint_id = self.env.context.get('orderpoint_id')
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
if not orderpoint:
|
||||
return
|
||||
orderpoint.route_id = self.env['stock.rule'].search([('action', '=', 'buy')], limit=1).route_id.id
|
||||
orderpoint.supplier_id = self
|
||||
if orderpoint.qty_to_order < self.min_qty:
|
||||
orderpoint.qty_to_order = self.min_qty
|
||||
|
||||
@@ -193,6 +193,10 @@ class Orderpoint(models.Model):
|
||||
""" Extend to add more depends values """
|
||||
return super()._compute_qty()
|
||||
|
||||
@api.depends('supplier_id')
|
||||
def _compute_lead_days(self):
|
||||
return super()._compute_lead_days()
|
||||
|
||||
@api.depends('route_id')
|
||||
def _compute_show_suppplier(self):
|
||||
buy_route = []
|
||||
@@ -216,6 +220,12 @@ class Orderpoint(models.Model):
|
||||
|
||||
return result
|
||||
|
||||
def _get_lead_days_values(self):
|
||||
values = super()._get_lead_days_values()
|
||||
if self.supplier_id:
|
||||
values['supplierinfo'] = self.supplier_id
|
||||
return values
|
||||
|
||||
def _get_replenishment_order_notification(self):
|
||||
self.ensure_one()
|
||||
order = self.env['purchase.order.line'].search([
|
||||
|
||||
@@ -141,16 +141,16 @@ class StockRule(models.Model):
|
||||
procurement.values, po))
|
||||
self.env['purchase.order.line'].sudo().create(po_line_values)
|
||||
|
||||
def _get_lead_days(self, product):
|
||||
def _get_lead_days(self, product, **values):
|
||||
"""Add the company security lead time, days to purchase and the supplier
|
||||
delay to the cumulative delay and cumulative description. The days to
|
||||
purchase and company lead time are always displayed for onboarding
|
||||
purpose in order to indicate that those options are available.
|
||||
"""
|
||||
delay, delay_description = super()._get_lead_days(product)
|
||||
delay, delay_description = super()._get_lead_days(product, **values)
|
||||
bypass_delay_description = self.env.context.get('bypass_delay_description')
|
||||
buy_rule = self.filtered(lambda r: r.action == 'buy')
|
||||
seller = product.with_company(buy_rule.company_id)._select_seller()
|
||||
seller = 'supplierinfo' in values and values['supplierinfo'] or product.with_company(buy_rule.company_id)._select_seller()
|
||||
if not buy_rule or not seller:
|
||||
return delay, delay_description
|
||||
buy_rule.ensure_one()
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
<field name="model">stock.warehouse.orderpoint</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="route_id" position="after">
|
||||
<xpath expr="//button[@name='action_stock_replenishment_info']" position="before">
|
||||
<field name="show_supplier" invisible="1"/>
|
||||
<field name="supplier_id" optional="hide" attrs="{'invisible': [('show_supplier', '=', False)]}" options="{'no_create': True}"/>
|
||||
</field>
|
||||
<field name="supplier_id" string="Vendor" optional="show" attrs="{'invisible': [('show_supplier', '=', False)]}" options="{'no_create': True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_replenishment_info
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockReplenishmentInfo(models.TransientModel):
|
||||
_inherit = 'stock.replenishment.info'
|
||||
_description = 'Stock supplier replenishment information'
|
||||
|
||||
supplierinfo_id = fields.Many2one(related='orderpoint_id.supplier_id')
|
||||
supplierinfo_ids = fields.Many2many(
|
||||
'product.supplierinfo', compute='_compute_supplierinfo_ids',
|
||||
store=True)
|
||||
|
||||
@api.depends('orderpoint_id')
|
||||
def _compute_supplierinfo_ids(self):
|
||||
for replenishment_info in self:
|
||||
replenishment_info.supplierinfo_ids = replenishment_info.product_id.seller_ids
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<record id="product_supplierinfo_replenishment_tree_view" model="ir.ui.view">
|
||||
<field name="name">product.supplierinfo.replenishment.tree.view</field>
|
||||
<field name="model">product.supplierinfo</field>
|
||||
<field name="inherit_id" ref="product.product_supplierinfo_tree_view"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="delay" position="after">
|
||||
<field name="show_set_supplier_button" invisible="1"/>
|
||||
<field name="last_purchase_date"/>
|
||||
<button name="action_set_supplier" type="object" string="Set as Supplier" class="btn btn-link"
|
||||
context="{'orderpoint_id': parent.orderpoint_id, 'stock_replenishment_info_id': parent.id}"
|
||||
attrs="{'invisible': [('show_set_supplier_button', '=', False)]}"/>
|
||||
</field>
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="decoration-bf">parent.supplierinfo_id == id</attribute>
|
||||
</xpath>
|
||||
<field name="min_qty" position="attributes">
|
||||
<attribute name="decoration-danger">min_qty > parent.qty_to_order</attribute>
|
||||
</field>
|
||||
<field name="company_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="delay" position="attributes">
|
||||
<attribute name="optional">show</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_replenishment_info_stock_purchase_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.replenishment.information.purchase.stock.inherit</field>
|
||||
<field name="model">stock.replenishment.info</field>
|
||||
<field name="inherit_id" ref="stock.view_stock_replenishment_info"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//footer" position="before">
|
||||
<field name="supplierinfo_id" invisible="1"/>
|
||||
<field name="supplierinfo_ids" readonly="1" context="{'tree_view_ref': 'purchase_stock.product_supplierinfo_replenishment_tree_view'}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -54,6 +54,7 @@
|
||||
'wizard/stock_backorder_confirmation_views.xml',
|
||||
'wizard/stock_quantity_history.xml',
|
||||
'wizard/stock_request_count.xml',
|
||||
'wizard/stock_replenishment_info.xml',
|
||||
'wizard/stock_rules_report_views.xml',
|
||||
'wizard/stock_warn_insufficient_qty_views.xml',
|
||||
'wizard/product_replenish_views.xml',
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections import defaultdict
|
||||
from datetime import datetime, time
|
||||
from dateutil import relativedelta
|
||||
from itertools import groupby
|
||||
from json import dumps
|
||||
from psycopg2 import OperationalError
|
||||
|
||||
from odoo import SUPERUSER_ID, _, api, fields, models, registry
|
||||
@@ -92,7 +91,6 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
allowed_location_ids = fields.One2many(comodel_name='stock.location', compute='_compute_allowed_location_ids')
|
||||
|
||||
rule_ids = fields.Many2many('stock.rule', string='Rules used', compute='_compute_rules')
|
||||
json_lead_days_popover = fields.Char(compute='_compute_json_popover')
|
||||
lead_days_date = fields.Date(compute='_compute_lead_days')
|
||||
allowed_route_ids = fields.Many2many('stock.location.route', compute='_compute_allowed_route_ids')
|
||||
route_id = fields.Many2one(
|
||||
@@ -127,36 +125,14 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
])
|
||||
self.allowed_route_ids = route_by_product.ids
|
||||
|
||||
@api.depends('rule_ids', 'product_id.seller_ids', 'product_id.seller_ids.delay')
|
||||
def _compute_json_popover(self):
|
||||
for orderpoint in self:
|
||||
if not orderpoint.product_id or not orderpoint.location_id:
|
||||
orderpoint.json_lead_days_popover = False
|
||||
continue
|
||||
dummy, lead_days_description = orderpoint.rule_ids._get_lead_days(orderpoint.product_id)
|
||||
orderpoint.json_lead_days_popover = dumps({
|
||||
'title': _('Replenishment'),
|
||||
'icon': 'fa-area-chart',
|
||||
'popoverTemplate': 'stock.leadDaysPopOver',
|
||||
'lead_days_date': fields.Date.to_string(orderpoint.lead_days_date),
|
||||
'lead_days_description': lead_days_description,
|
||||
'today': fields.Date.to_string(fields.Date.today()),
|
||||
'trigger': orderpoint.trigger,
|
||||
'qty_forecast': orderpoint.qty_forecast,
|
||||
'qty_to_order': orderpoint.qty_to_order,
|
||||
'product_min_qty': orderpoint.product_min_qty,
|
||||
'product_max_qty': orderpoint.product_max_qty,
|
||||
'product_uom_name': orderpoint.product_uom_name,
|
||||
'virtual': orderpoint.trigger == 'manual' and orderpoint.create_uid.id == SUPERUSER_ID,
|
||||
})
|
||||
|
||||
@api.depends('rule_ids', 'product_id.seller_ids', 'product_id.seller_ids.delay')
|
||||
def _compute_lead_days(self):
|
||||
for orderpoint in self:
|
||||
if not orderpoint.product_id or not orderpoint.location_id:
|
||||
orderpoint.lead_days_date = False
|
||||
continue
|
||||
lead_days, dummy = orderpoint.rule_ids._get_lead_days(orderpoint.product_id)
|
||||
values = orderpoint._get_lead_days_values()
|
||||
lead_days, dummy = orderpoint.rule_ids._get_lead_days(orderpoint.product_id, **values)
|
||||
lead_days_date = fields.Date.today() + relativedelta.relativedelta(days=lead_days)
|
||||
orderpoint.lead_days_date = lead_days_date
|
||||
|
||||
@@ -212,10 +188,32 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
|
||||
return super().write(vals)
|
||||
|
||||
def action_product_forecast_report(self):
|
||||
self.ensure_one()
|
||||
action = self.product_id.action_product_forecast_report()
|
||||
action['context'] = {
|
||||
'active_id': self.product_id.id,
|
||||
'active_model': 'product.product',
|
||||
}
|
||||
warehouse = self.warehouse_id
|
||||
if warehouse:
|
||||
action['context']['warehouse'] = warehouse.id
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def action_open_orderpoints(self):
|
||||
return self._get_orderpoint_action()
|
||||
|
||||
def action_stock_replenishment_info(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.actions']._for_xml_id('stock.action_stock_replenishment_info')
|
||||
action['name'] = _('Replenishment Information for %s in %s', self.product_id.display_name, self.warehouse_id.display_name)
|
||||
res = self.env['stock.replenishment.info'].create({
|
||||
'orderpoint_id': self.id,
|
||||
})
|
||||
action['res_id'] = res.id
|
||||
return action
|
||||
|
||||
def action_replenish(self):
|
||||
self._procure_orderpoint_confirm(company_id=self.env.company)
|
||||
notification = False
|
||||
@@ -288,6 +286,10 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
orderpoints = self.filtered(lambda o: o.location_id.id == g['location_id'][0])
|
||||
orderpoints.route_id = g['route_id']
|
||||
|
||||
def _get_lead_days_values(self):
|
||||
self.ensure_one()
|
||||
return dict()
|
||||
|
||||
def _get_product_context(self):
|
||||
"""Used to call `virtual_available` when running an orderpoint."""
|
||||
self.ensure_one()
|
||||
|
||||
@@ -320,7 +320,7 @@ class StockRule(models.Model):
|
||||
move_values[field] = values.get(field)
|
||||
return move_values
|
||||
|
||||
def _get_lead_days(self, product):
|
||||
def _get_lead_days(self, product, **values):
|
||||
"""Returns the cumulative delay and its description encountered by a
|
||||
procurement going through the rules in `self`.
|
||||
|
||||
|
||||
@@ -87,3 +87,4 @@ access_stock_inventory_conflict,stock.inventory.conflict,model_stock_inventory_c
|
||||
access_stock_inventory_warning,stock.inventory.warning,model_stock_inventory_warning,stock.group_stock_manager,1,1,1,0
|
||||
access_stock_inventory_adjustment_name,stock.inventory.adjustment.name,model_stock_inventory_adjustment_name,stock.group_stock_manager,1,1,1,0
|
||||
access_stock_request_count,stock.request.count,model_stock_request_count,stock.group_stock_manager,1,1,1,0
|
||||
access_stock_replenishment_info,stock.replenishment.info,model_stock_replenishment_info,stock.group_stock_manager,1,1,1,0
|
||||
|
||||
|
@@ -70,7 +70,21 @@ const ForecastWidgetField = AbstractField.extend({
|
||||
},
|
||||
});
|
||||
|
||||
const JsonWidget = AbstractField.extend({
|
||||
supportedFieldTypes: ['char'],
|
||||
|
||||
_render: function () {
|
||||
var value = JSON.parse(this.value);
|
||||
if (!value || !value.template) {
|
||||
this.$el.html('');
|
||||
return;
|
||||
}
|
||||
$(QWeb.render(value.template, value)).appendTo(this.$el);
|
||||
},
|
||||
});
|
||||
|
||||
fieldRegistry.add('forecast_widget', ForecastWidgetField);
|
||||
fieldRegistry.add('json_widget', JsonWidget);
|
||||
|
||||
return ForecastWidgetField;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ odoo.define('stock.StockOrderpointListController', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var ListController = require('web.ListController');
|
||||
var FormView = require('web.FormView');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
@@ -33,7 +34,7 @@ var StockOrderpointListController = ListController.extend({
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
_onButtonClicked: function (ev) {
|
||||
if (ev.data.attrs.class.split(' ').includes('o_replenish_buttons')) {
|
||||
if (ev.data.attrs.class && ev.data.attrs.class.split(' ').includes('o_replenish_buttons')) {
|
||||
ev.stopPropagation();
|
||||
var self = this;
|
||||
this._callButtonAction(ev.data.attrs, ev.data.record).then(function () {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="stock.leadDaysPopOver">
|
||||
<h2>Lead Times</h2>
|
||||
<hr/>
|
||||
<p>
|
||||
The forecasted stock on the <t t-esc="lead_days_date"/>
|
||||
is <t t-if="qty_to_order <= 0"><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></t><t t-else="">
|
||||
below the minimum inventory of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/>
|
||||
: <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished to reach the maximum of
|
||||
The forecasted stock on the <b><t t-esc="lead_days_date"/></b>
|
||||
is <t t-if="qty_to_order <= 0"><b><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></b></t><t t-else="">
|
||||
below the inventory <b>minimum of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/>
|
||||
: <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished</b> to reach the maximum of
|
||||
<t t-esc="product_max_qty"/> <t t-esc="product_uom_name"/>.</t>
|
||||
</p>
|
||||
<table t-if="lead_days_description" class="table table-borderless">
|
||||
<table t-if="lead_days_description" class="table table-borderless table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -29,11 +31,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="text-left btn btn-link action_open_forecast"
|
||||
type="button">
|
||||
<i class="fa fa-fw o_button_icon fa-arrow-right"></i>
|
||||
View Forecast
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<t t-name="StockOrderpoint.Buttons">
|
||||
@@ -47,4 +44,18 @@
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<div t-name="stock.replenishmentHistory">
|
||||
<h2>Sales History</h2>
|
||||
<hr/>
|
||||
<table t-if="replenishment_history" class="table table-borderless">
|
||||
<tbody>
|
||||
<t t-foreach="replenishment_history" t-as="line">
|
||||
<tr>
|
||||
<td><t t-esc="line.name"/></td>
|
||||
<td class="text-right text-nowrap" t-esc="line.quantity + ' ' + line.uom_name"/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
@@ -48,8 +48,9 @@
|
||||
<field name="warehouse_id" options="{'no_create': True}" groups="stock.group_stock_multi_warehouses" optional="hide"/>
|
||||
<field name="qty_on_hand" force_save="1"/>
|
||||
<field name="qty_forecast" force_save="1"/>
|
||||
<field name="json_lead_days_popover" optional="show" nolabel="1" string="Forecast Description" width="10px" widget="popover_widget" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<button name="action_product_forecast_report" type="object" icon="fa-area-chart" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<field name="route_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<button name="action_stock_replenishment_info" type="object" icon="fa-info" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<field name="trigger" optional="hide"/>
|
||||
<field name="product_min_qty" optional="show"/>
|
||||
<field name="product_max_qty" optional="show"/>
|
||||
@@ -83,7 +84,7 @@
|
||||
<field name="product_min_qty" optional="show"/>
|
||||
<field name="product_max_qty" optional="show"/>
|
||||
<field name="qty_multiple" optional="show"/>
|
||||
<field name="json_lead_days_popover" optional="show" nolabel="1" string="Forecast Description" width="10px" widget="popover_widget" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<button name="stock.action_stock_replenishment_info" string="Forecast Description" type="action" icon="fa-area-chart" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<field name="product_uom_name" string="UoM" groups="uom.group_uom"/>
|
||||
<field name="company_id" optional="hide" readonly="1" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
@@ -160,7 +161,7 @@
|
||||
<div class="o_row">
|
||||
<field name="product_min_qty"/>
|
||||
<field name="product_uom_name"/>
|
||||
<field name="json_lead_days_popover" widget="popover_widget" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<button name="stock.action_stock_replenishment_info" string="Forecast Description" type="action" icon="fa-area-chart" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
</div>
|
||||
<label for="product_max_qty"/>
|
||||
<div class="o_row">
|
||||
|
||||
@@ -18,3 +18,4 @@ from . import stock_track_confirmation
|
||||
from . import stock_package_destination
|
||||
from . import stock_orderpoint_snooze
|
||||
from . import stock_request_count
|
||||
from . import stock_replenishment_info
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from json import dumps
|
||||
from datetime import datetime, time
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID
|
||||
from odoo.osv.expression import AND
|
||||
from odoo.tools.date_utils import get_month, subtract
|
||||
|
||||
|
||||
class StockReplenishmentInfo(models.TransientModel):
|
||||
_name = 'stock.replenishment.info'
|
||||
_description = 'Stock supplier replenishment information'
|
||||
_rec_name = 'orderpoint_id'
|
||||
|
||||
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint')
|
||||
product_id = fields.Many2one('product.product', related='orderpoint_id.product_id')
|
||||
qty_to_order = fields.Float(related='orderpoint_id.qty_to_order')
|
||||
json_lead_days = fields.Char(compute='_compute_json_lead_days')
|
||||
json_replenishment_history = fields.Char(compute='_compute_json_replenishment_history')
|
||||
|
||||
@api.depends('orderpoint_id')
|
||||
def _compute_json_lead_days(self):
|
||||
for replenishment_report in self:
|
||||
if not replenishment_report.orderpoint_id.product_id or not replenishment_report.orderpoint_id.location_id:
|
||||
continue
|
||||
orderpoint = replenishment_report.orderpoint_id
|
||||
orderpoints_values = orderpoint._get_lead_days_values()
|
||||
dummy, lead_days_description = orderpoint.rule_ids._get_lead_days(
|
||||
orderpoint.product_id, **orderpoints_values)
|
||||
replenishment_report.json_lead_days = dumps({
|
||||
'template': 'stock.leadDaysPopOver',
|
||||
'lead_days_date': fields.Date.to_string(replenishment_report.orderpoint_id.lead_days_date),
|
||||
'lead_days_description': lead_days_description,
|
||||
'today': fields.Date.to_string(fields.Date.today()),
|
||||
'trigger': orderpoint.trigger,
|
||||
'qty_forecast': orderpoint.qty_forecast,
|
||||
'qty_to_order': orderpoint.qty_to_order,
|
||||
'product_min_qty': orderpoint.product_min_qty,
|
||||
'product_max_qty': orderpoint.product_max_qty,
|
||||
'product_uom_name': orderpoint.product_uom_name,
|
||||
'virtual': orderpoint.trigger == 'manual' and orderpoint.create_uid.id == SUPERUSER_ID,
|
||||
})
|
||||
|
||||
@api.depends('orderpoint_id')
|
||||
def _compute_json_replenishment_history(self):
|
||||
for replenishment_report in self:
|
||||
replenishment_history = []
|
||||
today = fields.Datetime.now()
|
||||
first_month = subtract(today, month=2)
|
||||
date_from, dummy = get_month(first_month)
|
||||
dummy, date_to = get_month(today)
|
||||
domain = [
|
||||
('product_id', '=', replenishment_report.product_id.id),
|
||||
('date', '>=', date_from),
|
||||
('date', '<=', datetime.combine(date_to, time.max)),
|
||||
('state', '=', 'done'),
|
||||
('company_id', '=', replenishment_report.orderpoint_id.company_id.id)
|
||||
]
|
||||
quantity_by_month_out = self.env['stock.move'].read_group(
|
||||
AND([domain, [('location_dest_id.usage', '=', 'customer')]]),
|
||||
['date', 'product_qty'], ['date:month'])
|
||||
quantity_by_month_returned = self.env['stock.move'].read_group(
|
||||
AND([domain, [('location_id.usage', '=', 'customer')]]),
|
||||
['date', 'product_qty'], ['date:month'])
|
||||
quantity_by_month_returned = {
|
||||
g['date:month']: g['product_qty'] for g in quantity_by_month_returned}
|
||||
for group in quantity_by_month_out:
|
||||
month = group['date:month']
|
||||
replenishment_history.append({
|
||||
'name': month,
|
||||
'quantity': group['product_qty'] - quantity_by_month_returned.get(month, 0),
|
||||
'uom_name': replenishment_report.product_id.uom_id.display_name,
|
||||
})
|
||||
replenishment_report.json_replenishment_history = dumps({
|
||||
'template': 'stock.replenishmentHistory',
|
||||
'replenishment_history': replenishment_history
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_stock_replenishment_info" model="ir.ui.view">
|
||||
<field name="name">Stock Replenishment Information</field>
|
||||
<field name="model">stock.replenishment.info</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="orderpoint_id" invisible="1"/>
|
||||
<field name="qty_to_order" invisible="1"/>
|
||||
<div class="row ml8">
|
||||
<group class="col-6">
|
||||
<field nolabel="1" name="json_lead_days" widget="json_widget"/>
|
||||
</group>
|
||||
<group class="col-6">
|
||||
<field nolabel="1" name="json_replenishment_history" widget="json_widget"/>
|
||||
</group>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Close" class="btn-default" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_stock_replenishment_info" model="ir.actions.act_window">
|
||||
<field name="name">Replenishment Information</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.replenishment.info</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_stock_replenishment_info"/>
|
||||
<field name="context">{'default_orderpoint_id': active_id}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user