[IMP] stock: improvements in replenishment wizard
With this commit: ======================== - Added a new field `forecasted_qty` in replenishment to display the forecasted qty based on a selected warehouse in the wizard - Changed the `route_ids`(m2m) field to `route_id`(m2o) so that we can apply a specific route for the replenishment instead of the product's default routes - Reduced the width of all the fields - Remove the time from the scheduled date and make the unit field not editable. - Can see vendor information with info icon field when buy is selected - Hide the Dropship route from the Route TaskId : 2579425 closes odoo/odoo#96948 Related: odoo/upgrade#3796 Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
@@ -20,7 +20,7 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
self.ensure_one()
|
||||
domain = [('orderpoint_id', 'in', self.ids)]
|
||||
if self.env.context.get('written_after'):
|
||||
domain = AND([domain, [('write_date', '>', self.env.context.get('written_after'))]])
|
||||
domain = AND([domain, [('write_date', '>=', self.env.context.get('written_after'))]])
|
||||
production = self.env['mrp.production'].search(domain, limit=1)
|
||||
if production:
|
||||
action = self.env.ref('mrp.action_mrp_production_form')
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import stock_picking_return
|
||||
from . import mrp_consumption_warning
|
||||
from . import change_production_qty
|
||||
from . import product_replenish
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class ProductReplenish(models.TransientModel):
|
||||
_inherit = 'product.replenish'
|
||||
|
||||
def _get_allowed_route_domain(self):
|
||||
domains = super()._get_allowed_route_domain()
|
||||
return expression.AND([domains, [('id', '!=', self.env.ref('mrp_subcontracting.route_resupply_subcontractor_mto', raise_if_not_found=False).id)]])
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import product_replenish
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class ProductReplenish(models.TransientModel):
|
||||
_inherit = 'product.replenish'
|
||||
|
||||
def _get_allowed_route_domain(self):
|
||||
domains = super()._get_allowed_route_domain()
|
||||
return expression.AND([domains, [('id', '!=', self.env.ref('mrp_subcontracting_dropshipping.route_subcontracting_dropshipping', raise_if_not_found=False).id)]])
|
||||
@@ -23,7 +23,8 @@
|
||||
'report/purchase_report_views.xml',
|
||||
'report/purchase_report_templates.xml',
|
||||
'report/report_stock_rule.xml',
|
||||
'wizard/stock_replenishment_info.xml'
|
||||
'wizard/stock_replenishment_info.xml',
|
||||
'wizard/product_replenish_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/purchase_stock_demo.xml',
|
||||
|
||||
@@ -130,3 +130,14 @@ class SupplierInfo(models.Model):
|
||||
orderpoint.supplier_id = self
|
||||
if orderpoint.qty_to_order < self.min_qty:
|
||||
orderpoint.qty_to_order = self.min_qty
|
||||
if self._context.get('replenish_id'):
|
||||
replenish = self.env['product.replenish'].browse(self._context.get('replenish_id'))
|
||||
replenish.supplier_id = self
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Replenish',
|
||||
'res_model': 'product.replenish',
|
||||
'res_id': replenish.id,
|
||||
'target': 'new',
|
||||
'view_mode': 'form',
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class Orderpoint(models.Model):
|
||||
self.ensure_one()
|
||||
domain = [('orderpoint_id', 'in', self.ids)]
|
||||
if self.env.context.get('written_after'):
|
||||
domain = AND([domain, [('write_date', '>', self.env.context.get('written_after'))]])
|
||||
domain = AND([domain, [('write_date', '>=', self.env.context.get('written_after'))]])
|
||||
order = self.env['purchase.order.line'].search(domain, limit=1).order_id
|
||||
if order:
|
||||
action = self.env.ref('purchase.action_rfq_form')
|
||||
|
||||
@@ -43,11 +43,14 @@ class TestReplenishWizard(TestStockCommon):
|
||||
'quantity': self.product_uom_qty,
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
('partner_id', '=', self.vendor.id)
|
||||
])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
self.assertTrue(last_po_id, 'Purchase Order not found')
|
||||
order_line = last_po_id.order_line.search([('product_id', '=', self.product1.id)])
|
||||
self.assertTrue(order_line, 'The product is not in the Purchase Order')
|
||||
@@ -93,10 +96,14 @@ class TestReplenishWizard(TestStockCommon):
|
||||
'quantity': 10,
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
self.assertEqual(last_po_id.partner_id, vendor1)
|
||||
self.assertEqual(last_po_id.order_line.price_unit, 100)
|
||||
|
||||
@@ -148,10 +155,14 @@ class TestReplenishWizard(TestStockCommon):
|
||||
'quantity': 10,
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
self.assertEqual(last_po_id.partner_id, vendor1)
|
||||
self.assertEqual(last_po_id.order_line.price_unit, 100)
|
||||
|
||||
@@ -193,10 +204,15 @@ class TestReplenishWizard(TestStockCommon):
|
||||
'quantity': 10,
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
|
||||
self.assertEqual(last_po_id.partner_id, vendor2)
|
||||
|
||||
def test_chose_supplier_4(self):
|
||||
@@ -241,10 +257,14 @@ class TestReplenishWizard(TestStockCommon):
|
||||
'quantity': 10,
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
|
||||
self.assertEqual(last_po_id.partner_id, vendor1)
|
||||
self.assertEqual(last_po_id.order_line.price_unit, 60)
|
||||
|
||||
@@ -11,12 +11,14 @@ from odoo.tests import Form
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.addons.stock.tests.common import TestStockCommon
|
||||
|
||||
|
||||
class TestStockValuation(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.url_extract_rec_id_and_model = TestStockCommon.url_extract_rec_id_and_model
|
||||
cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.partner_id = cls.env['res.partner'].create({
|
||||
@@ -107,12 +109,14 @@ class TestStockValuation(TransactionCase):
|
||||
'quantity': replenishment_uom_qty,
|
||||
'warehouse_id': self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1).id,
|
||||
})
|
||||
replenish_wizard.launch_replenishment()
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
purchase_order_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
last_po_id = False
|
||||
if purchase_order_id and model_name:
|
||||
last_po_id = self.env[model_name[0]].browse(int(purchase_order_id[0]))
|
||||
|
||||
last_po_id = self.env['purchase.order'].search([
|
||||
('origin', 'ilike', '%Manual Replenishment%'),
|
||||
('partner_id', '=', vendor.id)
|
||||
])[-1]
|
||||
order_line = last_po_id.order_line.search([('product_id', '=', self.product1.id)])
|
||||
self.assertEqual(order_line.product_qty,
|
||||
ap._compute_quantity(replenishment_uom_qty, kgm, rounding_method='HALF-UP'),
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_replenishment_info
|
||||
from . import product_replenish
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
class ProductReplenish(models.TransientModel):
|
||||
_inherit = 'product.replenish'
|
||||
|
||||
supplier_id = fields.Many2one("product.supplierinfo", string="Vendor")
|
||||
show_vendor = fields.Boolean(compute="_compute_show_vendor")
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if res.get('product_id'):
|
||||
product_id = self.env['product.product'].browse(res['product_id'])
|
||||
product_tmpl_id = product_id.product_tmpl_id
|
||||
if 'warehouse_id' not in res:
|
||||
company = product_tmpl_id.company_id or self.env.company
|
||||
res['warehouse_id'] = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1).id
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', 'in', [product_tmpl_id.product_variant_id.id, product_id.id]), ("warehouse_id", "=", res['warehouse_id'])], limit=1)
|
||||
if orderpoint:
|
||||
res['supplier_id'] = orderpoint.supplier_id.id
|
||||
elif product_tmpl_id.seller_ids:
|
||||
res['supplier_id'] = product_tmpl_id.seller_ids[0].id
|
||||
if not product_tmpl_id.seller_ids:
|
||||
company = product_tmpl_id.company_id or self.env.company
|
||||
domain = ['|', ('company_id', '=', False), ('company_id', '=', company.id)] + self._get_allowed_route_domain()
|
||||
domain = AND([domain, [('id', '!=', self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False).id)]])
|
||||
res['route_id'] = self.env['stock.route'].search(domain, limit=1).id
|
||||
return res
|
||||
|
||||
@api.depends('route_id')
|
||||
def _compute_show_vendor(self):
|
||||
for rec in self:
|
||||
rec.show_vendor = rec.route_id == self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False)
|
||||
|
||||
@api.onchange('route_id')
|
||||
def _onchange_route_id(self):
|
||||
for rec in self:
|
||||
if rec.route_id == self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False) and not rec.product_id.product_tmpl_id.seller_ids:
|
||||
return {
|
||||
'warning': {
|
||||
'title': _("Vendor Not Found in Product %s", rec.product_id.name),
|
||||
'message': _("Go on the product form and add the list of vendors"),
|
||||
},
|
||||
}
|
||||
|
||||
def _prepare_orderpoint_values(self):
|
||||
res = super()._prepare_orderpoint_values()
|
||||
if self.supplier_id:
|
||||
res['supplier_id'] = self.supplier_id.id
|
||||
return res
|
||||
|
||||
def action_stock_replenishment_info(self):
|
||||
self.ensure_one()
|
||||
orderpoint = self.env["stock.warehouse.orderpoint"].search([("product_id", "=", self.product_id.id), ("warehouse_id", "=", self.warehouse_id.id)], limit=1)
|
||||
if not orderpoint:
|
||||
orderpoint = self.env["stock.warehouse.orderpoint"].create({
|
||||
"product_id": self.product_id.id,
|
||||
"warehouse_id": self.warehouse_id.id,
|
||||
})
|
||||
action = orderpoint.action_stock_replenishment_info()
|
||||
|
||||
action["context"] = {
|
||||
'default_orderpoint_id': orderpoint.id,
|
||||
'replenish_id': self.id,
|
||||
}
|
||||
return action
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_product_replenish_form_inherit_stock" model="ir.ui.view">
|
||||
<field name="name">product.replenish.form.inherit.stock</field>
|
||||
<field name="model">product.replenish</field>
|
||||
<field name="inherit_id" ref="stock.view_product_replenish"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='route_id']" position="after">
|
||||
<label for="supplier_id" attrs="{'invisible': [('show_vendor', '=', False)]}"/>
|
||||
<div class="o_row">
|
||||
<field name="show_vendor" invisible="1"/>
|
||||
<field name="supplier_id" attrs="{'invisible': [('show_vendor', '=', False)], 'required': [('show_vendor', '=', True)]}" domain="[('product_tmpl_id', '=', product_tmpl_id)]" options="{'no_open': 1, 'no_create': 1}"/>
|
||||
<button name="action_stock_replenishment_info"
|
||||
type="object"
|
||||
title="Show Vendor"
|
||||
icon="fa-info-circle"
|
||||
attrs="{'invisible': [('show_vendor', '=', False)]}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -17,3 +17,21 @@ class StockReplenishmentInfo(models.TransientModel):
|
||||
def _compute_supplierinfo_ids(self):
|
||||
for replenishment_info in self:
|
||||
replenishment_info.supplierinfo_ids = replenishment_info.product_id.seller_ids
|
||||
|
||||
|
||||
class StockReplenishmentOption(models.TransientModel):
|
||||
_inherit = 'stock.replenishment.option'
|
||||
|
||||
def select_route(self):
|
||||
if self._context.get('replenish_id'):
|
||||
replenish = self.env['product.replenish'].browse(self._context.get('replenish_id'))
|
||||
replenish.route_id = self.route_id.id
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Replenish',
|
||||
'res_model': 'product.replenish',
|
||||
'res_id': replenish.id,
|
||||
'target': 'new',
|
||||
'view_mode': 'form',
|
||||
}
|
||||
return super().select_route()
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<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">
|
||||
@@ -38,7 +37,7 @@
|
||||
<xpath expr="//page" position="before">
|
||||
<page string="Vendors">
|
||||
<field name="supplierinfo_id" invisible="1"/>
|
||||
<field name="supplierinfo_ids" readonly="1" context="{'tree_view_ref': 'purchase_stock.product_supplierinfo_replenishment_tree_view'}"/>
|
||||
<field name="supplierinfo_ids" readonly="1" context="{'tree_view_ref': 'purchase_stock.product_supplierinfo_replenishment_tree_view', 'stock_replenishment_info_id': id, 'orderpoint_id': orderpoint_id}"/>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -180,7 +180,7 @@ class StockMove(models.Model):
|
||||
display_clear_serial = fields.Boolean(compute='_compute_display_clear_serial')
|
||||
next_serial = fields.Char('First SN')
|
||||
next_serial_count = fields.Integer('Number of SN')
|
||||
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Original Reordering Rule', check_company=True, index=True)
|
||||
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Original Reordering Rule', index=True)
|
||||
forecast_availability = fields.Float('Forecast Availability', compute='_compute_forecast_information', digits='Product Unit of Measure', compute_sudo=True)
|
||||
forecast_expected_date = fields.Datetime('Forecasted Expected date', compute='_compute_forecast_information', compute_sudo=True)
|
||||
lot_ids = fields.Many2many('stock.lot', compute='_compute_lot_ids', inverse='_set_lot_ids', string='Serial Numbers', readonly=False)
|
||||
|
||||
@@ -224,7 +224,7 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
return action
|
||||
|
||||
def action_replenish(self):
|
||||
now = datetime.now()
|
||||
now = self.env.cr.now()
|
||||
try:
|
||||
self._procure_orderpoint_confirm(company_id=self.env.company)
|
||||
except UserError as e:
|
||||
@@ -444,14 +444,20 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
self.ensure_one()
|
||||
domain = [('orderpoint_id', 'in', self.ids)]
|
||||
if self.env.context.get('written_after'):
|
||||
domain = expression.AND([domain, [('write_date', '>', self.env.context.get('written_after'))]])
|
||||
domain = expression.AND([domain, [('write_date', '>=', self.env.context.get('written_after'))]])
|
||||
move = self.env['stock.move'].search(domain, limit=1)
|
||||
if move.picking_id:
|
||||
action = self.env.ref('stock.stock_picking_action_picking_type')
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('The inter-warehouse transfers have been generated'),
|
||||
'message': '%s',
|
||||
'links': [{
|
||||
'label': move.picking_id.name,
|
||||
'url': f'#action={action.id}&id={move.picking_id.id}&model=stock.picking&view_type=form'
|
||||
}],
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
@@ -95,3 +96,8 @@ class TestStockCommon(common.TransactionCase):
|
||||
cls.UnitA = cls.ProductObj.create({'name': 'Unit-A', 'type': 'product'})
|
||||
cls.kgB = cls.ProductObj.create({'name': 'kg-B', 'type': 'product', 'uom_id': cls.uom_kg.id, 'uom_po_id': cls.uom_kg.id})
|
||||
cls.gB = cls.ProductObj.create({'name': 'g-B', 'type': 'product', 'uom_id': cls.uom_gm.id, 'uom_po_id': cls.uom_gm.id})
|
||||
|
||||
def url_extract_rec_id_and_model(self, url):
|
||||
rec_id = re.findall(r'[?&]id=([^&]+).*', url)
|
||||
model_name = re.findall(r'[?&]model=([^&]+).*', url)
|
||||
return rec_id, model_name
|
||||
|
||||
@@ -2722,8 +2722,14 @@ class TestRoutes(TestStockCommon):
|
||||
'warehouse_id': self.wh.id,
|
||||
})
|
||||
|
||||
replenish_wizard.launch_replenishment()
|
||||
last_picking_id = self.env['stock.picking'].search([('origin', '=', 'Manual Replenishment')])[-1]
|
||||
genrated_picking = replenish_wizard.launch_replenishment()
|
||||
links = genrated_picking.get("params", {}).get("links")
|
||||
url = links and links[0].get("url", "") or ""
|
||||
picking_id, model_name = self.url_extract_rec_id_and_model(url)
|
||||
|
||||
last_picking_id = False
|
||||
if picking_id and model_name:
|
||||
last_picking_id = self.env[model_name[0]].browse(int(picking_id[0]))
|
||||
self.assertTrue(last_picking_id, 'Picking not found')
|
||||
move_line = last_picking_id.move_ids.search([('product_id', '=', self.product1.id)])
|
||||
self.assertTrue(move_line,'The product is not in the picking')
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
|
||||
@@ -16,22 +17,25 @@ class ProductReplenish(models.TransientModel):
|
||||
product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True)
|
||||
product_has_variants = fields.Boolean('Has variants', default=False, required=True)
|
||||
product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id', readonly=True, required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unit of measure', required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unity of measure', required=True)
|
||||
forecast_uom_id = fields.Many2one(related='product_id.uom_id')
|
||||
quantity = fields.Float('Quantity', default=1, required=True)
|
||||
date_planned = fields.Datetime('Scheduled Date', required=True, help="Date at which the replenishment should take place.")
|
||||
warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', string='Warehouse', required=True,
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
route_ids = fields.Many2many(
|
||||
'stock.route', string='Preferred Routes',
|
||||
help="Apply specific route(s) for the replenishment instead of product's default routes.",
|
||||
route_id = fields.Many2one(
|
||||
'stock.route', string='Preferred Route',
|
||||
help="Apply specific route for the replenishment instead of product's default routes.",
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
company_id = fields.Many2one('res.company')
|
||||
forecasted_quantity = fields.Float(string="Forecasted Quantity", compute="_compute_forecasted_quantity")
|
||||
allowed_route_ids = fields.Many2many("stock.route", compute="_compute_allowed_route_ids")
|
||||
|
||||
@api.onchange('product_id')
|
||||
@api.onchange('product_id', 'warehouse_id')
|
||||
def _onchange_product_id(self):
|
||||
if not self.env.context.get('default_quantity'):
|
||||
self.quantity = abs(self.product_id.virtual_available) if self.product_id.virtual_available < 0 else 1
|
||||
self.quantity = abs(self.forecasted_quantity) if self.forecasted_quantity < 0 else 1
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
@@ -60,38 +64,65 @@ class ProductReplenish(models.TransientModel):
|
||||
res['warehouse_id'] = warehouse.id
|
||||
if 'date_planned' in fields:
|
||||
res['date_planned'] = datetime.datetime.now()
|
||||
if 'route_id' in fields and 'route_id' not in res:
|
||||
route_id = False
|
||||
domain = expression.AND([self._get_allowed_route_domain(), ['|', ('company_id', '=', False), ('company_id', '=', company.id)]])
|
||||
if product_tmpl_id.route_ids:
|
||||
product_route_domain = expression.AND([domain, [('product_ids', '=', product_tmpl_id.id)]])
|
||||
route_id = self.env['stock.route'].search(product_route_domain, limit=1).id
|
||||
if not route_id:
|
||||
route_id = self.env['stock.route'].search(domain, limit=1).id
|
||||
if route_id:
|
||||
res['route_id'] = route_id
|
||||
return res
|
||||
|
||||
def launch_replenishment(self):
|
||||
uom_reference = self.product_id.uom_id
|
||||
self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference, rounding_method='HALF-UP')
|
||||
try:
|
||||
self.env['procurement.group'].with_context(clean_context(self.env.context)).run([
|
||||
self.env['procurement.group'].Procurement(
|
||||
self.product_id,
|
||||
self.quantity,
|
||||
uom_reference,
|
||||
self.warehouse_id.lot_stock_id, # Location
|
||||
_("Manual Replenishment"), # Name
|
||||
_("Manual Replenishment"), # Origin
|
||||
self.warehouse_id.company_id,
|
||||
self._prepare_run_values() # Values
|
||||
)
|
||||
])
|
||||
return {
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', self.product_id.id)])
|
||||
if orderpoint:
|
||||
orderpoint.write(self._prepare_orderpoint_values())
|
||||
else:
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create(self._prepare_orderpoint_values())
|
||||
notification = orderpoint.action_replenish()
|
||||
act_window_close = {
|
||||
'type': 'ir.actions.act_window_close',
|
||||
'infos': {'done': True},
|
||||
}
|
||||
if notification:
|
||||
notification['params']['next'] = act_window_close
|
||||
return notification
|
||||
return act_window_close
|
||||
except UserError as error:
|
||||
raise UserError(error)
|
||||
|
||||
def _prepare_run_values(self):
|
||||
replenishment = self.env['procurement.group'].create({})
|
||||
|
||||
def _prepare_orderpoint_values(self):
|
||||
values = {
|
||||
'warehouse_id': self.warehouse_id,
|
||||
'route_ids': self.route_ids,
|
||||
'date_planned': self.date_planned,
|
||||
'group_id': replenishment,
|
||||
'location_id': self.warehouse_id.lot_stock_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'qty_to_order': self.quantity,
|
||||
}
|
||||
if self.route_id:
|
||||
values['route_id'] = self.route_id.id
|
||||
return values
|
||||
|
||||
@api.depends('warehouse_id', 'product_id')
|
||||
def _compute_forecasted_quantity(self):
|
||||
for rec in self:
|
||||
rec.forecasted_quantity = rec.product_id.with_context(warehouse=rec.warehouse_id.id).virtual_available
|
||||
|
||||
# OVERWRITE in 'Drop Shipping', 'Dropship and Subcontracting Management' and 'Dropship and Subcontracting Management' to hide it
|
||||
def _get_allowed_route_domain(self):
|
||||
stock_location_inter_wh_id = self.env.ref('stock.stock_location_inter_wh').id
|
||||
return [
|
||||
('product_selectable', '=', True),
|
||||
('rule_ids.location_src_id', '!=', stock_location_inter_wh_id),
|
||||
('rule_ids.location_dest_id', '!=', stock_location_inter_wh_id)
|
||||
]
|
||||
|
||||
@api.depends('product_id', 'product_tmpl_id')
|
||||
def _compute_allowed_route_ids(self):
|
||||
domain = self._get_allowed_route_domain()
|
||||
route_ids = self.env['stock.route'].search(domain)
|
||||
self.allowed_route_ids = route_ids
|
||||
|
||||
@@ -11,26 +11,36 @@
|
||||
a manufacturing order or a transfer.
|
||||
</p>
|
||||
<group>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
<field name="product_has_variants" invisible="1"/>
|
||||
<field name="product_id"
|
||||
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
|
||||
attrs="{'readonly': [('product_has_variants', '=', False)]}"
|
||||
options="{'no_create_edit':1}"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<label for="quantity"/>
|
||||
<div class="o_row">
|
||||
<field name="quantity" />
|
||||
<field name="product_uom_id"
|
||||
domain="[('category_id', '=', product_uom_category_id)]"
|
||||
groups="uom.group_uom"/>
|
||||
</div>
|
||||
<field name="date_planned"/>
|
||||
<field name="warehouse_id"
|
||||
groups="stock.group_stock_multi_warehouses"/>
|
||||
<field name="route_ids"
|
||||
widget="many2many_tags"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
<field name="product_has_variants" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="allowed_route_ids" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_id"
|
||||
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
|
||||
attrs="{'readonly': [('product_has_variants', '=', False)]}"
|
||||
options="{'no_open': 1, 'no_create': 1}"/>
|
||||
<label for="quantity"/>
|
||||
<div class="o_row">
|
||||
<field name="quantity" />
|
||||
<field name="product_uom_id" groups="!uom.group_uom" invisible="1"/>
|
||||
<field name="product_uom_id"
|
||||
domain="[('category_id', '=', product_uom_category_id)]"
|
||||
groups="uom.group_uom"
|
||||
options="{'no_open': 1, 'no_create': 1}"/>
|
||||
</div>
|
||||
<label for="forecasted_quantity"/>
|
||||
<div class="o_row">
|
||||
<field name="forecasted_quantity" readonly="1"/>
|
||||
<field name="forecast_uom_id" groups="!uom.group_uom" invisible="1"/>
|
||||
<field name="forecast_uom_id" domain="[('category_id', '=', product_uom_category_id)]" groups="uom.group_uom" readonly="1"/>
|
||||
</div>
|
||||
<field name="date_planned" widget="date"/>
|
||||
<field name="warehouse_id"
|
||||
groups="stock.group_stock_multi_warehouses"/>
|
||||
<field name="route_id" domain="[('id', 'in', allowed_route_ids)]"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="launch_replenishment"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button string="Close" class="btn-default" special="cancel" data-hotkey="z"/>
|
||||
<button string="Close" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import product_replenish
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class ProductReplenish(models.TransientModel):
|
||||
_inherit = 'product.replenish'
|
||||
|
||||
def _get_allowed_route_domain(self):
|
||||
domains = super()._get_allowed_route_domain()
|
||||
return expression.AND([domains, [('id', '!=', self.env.ref('stock_dropshipping.route_drop_shipping', raise_if_not_found=False).id)]])
|
||||
Reference in New Issue
Block a user