Начальное наполнение

This commit is contained in:
parent 8a1014b90b
commit 9f0c6b446c
288 changed files with 698400 additions and 0 deletions

30
__init__.py Normal file
View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import report
from . import wizard
from . import populate
# TODO: Apply proper fix & remove in master
def pre_init_hook(env):
env['ir.model.data'].search([
('model', 'like', 'stock'),
('module', '=', 'stock')
]).unlink()
def _assign_default_mail_template_picking_id(env):
company_ids_without_default_mail_template_id = env['res.company'].search([
('stock_mail_confirmation_template_id', '=', False)
])
default_mail_template_id = env.ref('stock.mail_template_data_delivery_confirmation', raise_if_not_found=False)
if default_mail_template_id:
company_ids_without_default_mail_template_id.write({
'stock_mail_confirmation_template_id': default_mail_template_id.id,
})
def uninstall_hook(env):
picking_type_ids = env["stock.picking.type"].with_context({"active_test": False}).search([])
picking_type_ids.sequence_id.unlink()

121
__manifest__.py Normal file
View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Inventory',
'version': '1.1',
'summary': 'Manage your stock and logistics activities',
'website': 'https://www.odoo.com/app/inventory',
'depends': ['product', 'barcodes_gs1_nomenclature', 'digest'],
'category': 'Inventory/Inventory',
'sequence': 25,
'demo': [
'data/stock_demo_pre.xml',
'data/stock_demo.xml',
'data/stock_demo2.xml',
'data/stock_orderpoint_demo.xml',
'data/stock_storage_category_demo.xml',
],
'data': [
'security/stock_security.xml',
'security/ir.model.access.csv',
'data/digest_data.xml',
'data/mail_templates.xml',
'data/default_barcode_patterns.xml',
'data/stock_data.xml',
'data/stock_sequence_data.xml',
'data/stock_traceability_report_data.xml',
'report/report_stock_quantity.xml',
'report/report_stock_reception.xml',
'report/stock_report_views.xml',
'report/report_package_barcode.xml',
'report/report_lot_barcode.xml',
'report/report_location_barcode.xml',
'report/report_stockpicking_operations.xml',
'report/report_deliveryslip.xml',
'report/report_stockinventory.xml',
'report/report_stock_rule.xml',
'report/package_templates.xml',
'report/picking_templates.xml',
'report/product_templates.xml',
'report/product_packaging.xml',
'report/report_return_slip.xml',
'data/mail_template_data.xml',
'views/stock_menu_views.xml',
'wizard/stock_assign_serial_views.xml',
'wizard/stock_change_product_qty_views.xml',
'wizard/stock_picking_return_views.xml',
'wizard/stock_scheduler_compute_views.xml',
'wizard/stock_inventory_conflict.xml',
'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',
'wizard/product_label_layout_views.xml',
'wizard/stock_track_confirmation_views.xml',
'wizard/stock_orderpoint_snooze_views.xml',
'wizard/stock_package_destination_views.xml',
'wizard/stock_inventory_adjustment_name.xml',
'wizard/stock_inventory_warning.xml',
'wizard/stock_label_type.xml',
'wizard/stock_lot_label_layout.xml',
'wizard/stock_quant_relocate.xml',
'views/res_partner_views.xml',
'views/product_strategy_views.xml',
'views/stock_lot_views.xml',
'views/stock_scrap_views.xml',
'views/stock_quant_views.xml',
'views/stock_warehouse_views.xml',
'views/stock_move_line_views.xml',
'views/stock_picking_views.xml',
'views/stock_picking_type_views.xml',
'views/stock_move_views.xml',
'views/product_views.xml',
'views/stock_location_views.xml',
'views/stock_orderpoint_views.xml',
'views/stock_storage_category_views.xml',
'views/res_config_settings_views.xml',
'views/report_stock_traceability.xml',
'views/stock_template.xml',
'views/stock_rule_views.xml',
'views/stock_package_level_views.xml',
'views/stock_package_type_view.xml',
'views/stock_forecasted.xml',
],
'installable': True,
'application': True,
'pre_init_hook': 'pre_init_hook',
'post_init_hook': '_assign_default_mail_template_picking_id',
'uninstall_hook': 'uninstall_hook',
'assets': {
'web.report_assets_common': [
'stock/static/src/scss/report_stock_reception.scss',
'stock/static/src/scss/report_stock_rule.scss',
],
'web.assets_backend': [
'stock/static/src/**/*.js',
'stock/static/src/**/*.xml',
'stock/static/src/scss/*.scss',
'stock/static/src/views/**/*',
],
'web.assets_frontend': [
'stock/static/src/scss/stock_traceability_report.scss',
],
'web.assets_tests': [
'stock/static/tests/tours/*.js',
],
'web.qunit_suite_tests': [
'stock/static/tests/inventory_report_list_tests.js',
'stock/static/tests/popover_widget_tests.js',
'stock/static/tests/stock_traceability_report_backend_tests.js',
],
},
'license': 'LGPL-3',
}

1
controllers/__init__.py Normal file
View File

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

38
controllers/main.py Normal file
View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
import werkzeug
from werkzeug.exceptions import InternalServerError
from odoo import http
from odoo.http import request
from odoo.tools.misc import html_escape
import json
class StockReportController(http.Controller):
@http.route('/stock/<string:output_format>/<string:report_name>', type='http', auth='user')
def report(self, output_format, report_name=False, **kw):
uid = request.session.uid
domain = [('create_uid', '=', uid)]
stock_traceability = request.env['stock.traceability.report'].with_user(uid).search(domain, limit=1)
line_data = json.loads(kw['data'])
try:
if output_format == 'pdf':
response = request.make_response(
stock_traceability.with_context(active_id=kw['active_id'], active_model=kw['active_model']).get_pdf(line_data),
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', 'attachment; filename=' + 'stock_traceability' + '.pdf;')
]
)
return response
except Exception as e:
se = http.serialize_exception(e)
error = {
'code': 200,
'message': 'Odoo Server Error',
'data': se
}
res = request.make_response(html_escape(json.dumps(error)))
raise InternalServerError(response=res) from e

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="barcode_rule_weight_three_dec" model="barcode.rule">
<field name="name">Weight Barcodes 3 Decimals</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="sequence">36</field>
<field name="type">weight</field>
<field name="encoding">ean13</field>
<field name="pattern">21.....{NNDDD}</field>
</record>
<record id="barcode_rule_package" model="barcode.rule">
<field name="name">Package barcodes</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="sequence">70</field>
<field name="type">package</field>
<field name="encoding">any</field>
<field name="pattern">PACK</field>
</record>
<record id="barcode_rule_lot" model="barcode.rule">
<field name="name">Lot barcodes</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="sequence">80</field>
<field name="type">lot</field>
<field name="encoding">any</field>
<field name="pattern">10</field>
</record>
<record id="barcode_rule_location" model="barcode.rule">
<field name="name">Location barcodes</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="sequence">60</field>
<field name="type">location</field>
<field name="encoding">any</field>
<field name="pattern">414</field>
</record>
</data>
</odoo>

26
data/digest_data.xml Normal file
View File

@ -0,0 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<record id="digest_tip_stock_0" model="digest.tip">
<field name="name">Tip: Speed up inventory operations with barcodes</field>
<field name="sequence">1000</field>
<field name="group_id" ref="stock.group_stock_user" />
<field name="tip_description" type="html">
<div>
<p class="tip_title">Tip: Speed up inventory operations with barcodes</p>
<table class="tip_twocol">
<tr>
<td class="tip_twocol_left" style="width: 45%;">
<img src="https://download.odoocdn.com/digests/stock/static/src/img/barcode.gif" class="tip_twocol_img" width="240"/>
</td>
<td style="width: 55%;">
<p class="tip_content" style="margin: 0;">Enjoy a quick-paced experience with the Odoo barcode app. It is blazing fast and works even without a stable internet connection. It supports all flows: inventory adjustments, batch picking, moving lots or pallets, low inventory checks, etc. Go to the "Apps" menu to activate the barcode interface.</p>
</td>
</tr>
</table>
<div style="clear: both;" />
</div>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,48 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo><data noupdate="1">
<record id="mail_template_data_delivery_confirmation" model="mail.template">
<field name="name">Shipping: Send by Email</field>
<field name="model_id" ref="model_stock_picking"/>
<field name="subject">{{ object.company_id.name }} Delivery Order (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.email and object.partner_id.id or object.partner_id.parent_id.id }}</field>
<field name="description">Sent to the customers when orders are delivered, if the setting is enabled</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello <t t-out="object.partner_id.name or ''">Brandon Freeman</t>,<br/><br/>
We are glad to inform you that your order has been shipped.
<t t-if="hasattr(object, 'carrier_tracking_ref') and object.carrier_tracking_ref">
Your tracking reference is
<strong>
<t t-if="object.carrier_tracking_url">
<t t-set="multiple_carrier_tracking" t-value="object.get_multiple_carrier_tracking()"/>
<t t-if="multiple_carrier_tracking">
<t t-foreach="multiple_carrier_tracking" t-as="line">
<br/><a t-att-href="line[1]" target="_blank" t-out="line[0] or ''"></a>
</t>
</t>
<t t-else="">
<a t-attf-href="{{ object.carrier_tracking_url }}" target="_blank" t-out="object.carrier_tracking_ref or ''"></a>.
</t>
</t>
<t t-else="">
<t t-out="object.carrier_tracking_ref or ''"></t>.
</t>
</strong>
</t>
<br/><br/>
Please find your delivery order attached for more details.<br/><br/>
Thank you,
<t t-if="user.signature">
<br />
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
</t>
</p>
</div>
</field>
<field name="report_template_ids" eval="[(4, ref('stock.action_report_delivery'))]"/>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

32
data/mail_templates.xml Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="track_move_template">
<div>
<t t-call="stock.message_head"/>
<t t-call="stock.message_body"/>
</div>
</template>
<template id="exception_on_picking">
<div> Exception(s) occurred on the picking
<a href="#" data-oe-model="stock.picking" t-att-data-oe-id="origin_picking.id"><t t-esc="origin_picking.name"/></a>.
Manual actions may be needed.
<div class="mt16">
<p>Exception(s):</p>
<ul t-foreach="moves_information" t-as="exception">
<t t-set="move" t-value="exception[0]"/>
<t t-set="new_qty" t-value="exception[1][0]"/>
<t t-set="old_qty" t-value="exception[1][1]"/>
<li><t t-esc="new_qty"/> <t t-esc="move.product_uom.name"/>
of <t t-esc="move.product_id.display_name"/> processed instead of <t t-esc="old_qty"/> <t t-esc="move.product_uom.name"/></li>
</ul>
</div>
<div class="mt16" t-if="impacted_pickings">
<p>Next transfer(s) impacted:</p>
<ul t-foreach="impacted_pickings" t-as="picking">
<li><a href="#" data-oe-model="stock.picking" t-att-data-oe-id="picking.id"><t t-esc="picking.name"/></a></li>
</ul>
</div>
</div>
</template>
</odoo>

130
data/stock_data.xml Normal file
View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="removal_fifo" model="product.removal">
<field name="name">First In First Out (FIFO)</field>
<field name="method">fifo</field>
</record>
<record id="removal_lifo" model="product.removal">
<field name="name">Last In First Out (LIFO)</field>
<field name="method">lifo</field>
</record>
<record id="removal_closest" model="product.removal">
<field name="name">Closest Location</field>
<field name="method">closest</field>
</record>
<record id="removal_least_packages" model="product.removal">
<field name="name">Least Packages</field>
<field name="method">least_packages</field>
</record>
</data>
<data noupdate="1">
<!-- Resource: stock.location -->
<record id="stock_location_locations" model="stock.location">
<field name="name">Physical Locations</field>
<field name="usage">view</field>
<field name="company_id"></field>
</record>
<record id="stock_location_locations_partner" model="stock.location">
<field name="name">Partners</field>
<field name="usage">view</field>
<field name="posz">1</field>
<field name="company_id"></field>
</record>
<record id="stock_location_locations_virtual" model="stock.location">
<field name="name">Virtual Locations</field>
<field name="usage">view</field>
<field name="posz">1</field>
<field name="company_id"></field>
</record>
<record id="stock_location_suppliers" model="stock.location">
<field name="name">Vendors</field>
<field name="location_id" ref="stock_location_locations_partner"/>
<field name="usage">supplier</field>
<field name="return_location">True</field>
<field name="company_id"></field>
</record>
<record id="stock_location_customers" model="stock.location">
<field name="name">Customers</field>
<field name="location_id" ref="stock_location_locations_partner"/>
<field name="usage">customer</field>
<field name="company_id"></field>
</record>
<record id="stock_location_inter_wh" model="stock.location">
<field name="name">Inter-company transit</field>
<field name="location_id" ref="stock_location_locations_virtual"/>
<field name="usage">transit</field>
<field name="company_id"></field>
<field name="active" eval="False"/>
</record>
<!-- set a lower sequence on the mto route than on the resupply routes -->
<record id="route_warehouse0_mto" model='stock.route'>
<field name="name">Replenish on Order (MTO)</field>
<field name="company_id"></field>
<field name="active">False</field>
<field name="sequence">5</field>
</record>
<!-- Properties -->
<record forcecreate="True" id="property_stock_supplier" model="ir.property">
<field name="name">property_stock_supplier</field>
<field name="fields_id" search="[('model','=','res.partner'),('name','=','property_stock_supplier')]"/>
<field eval="'stock.location,'+str(stock_location_suppliers)" name="value"/>
</record>
<record forcecreate="True" id="property_stock_customer" model="ir.property">
<field name="name">property_stock_customer</field>
<field name="fields_id" search="[('model','=','res.partner'),('name','=','property_stock_customer')]"/>
<field eval="'stock.location,'+str(stock_location_customers)" name="value"/>
</record>
<!-- Resource: stock.warehouse -->
<record id="warehouse0" model="stock.warehouse">
<field name="partner_id" ref="base.main_partner"/>
<field name="code">WH</field>
</record>
<!-- create xml ids for demo data that are widely used in tests or in other codes, for more convenience -->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'stock.stock_location_stock',
'record': obj().env.ref('stock.warehouse0').lot_stock_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_company',
'record': obj().env.ref('stock.warehouse0').wh_input_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_output',
'record': obj().env.ref('stock.warehouse0').wh_output_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.location_pack_zone',
'record': obj().env.ref('stock.warehouse0').wh_pack_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_internal',
'record': obj().env.ref('stock.warehouse0').int_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_in',
'record': obj().env.ref('stock.warehouse0').in_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_out',
'record': obj().env.ref('stock.warehouse0').out_type_id,
'noupdate': True,
}]"/>
</function>
<!-- create the transit location for each company existing -->
<function model="res.company" name="create_missing_transit_location"/>
<function model="res.company" name="create_missing_warehouse"/>
<function model="res.company" name="create_missing_inventory_loss_location"/>
<function model="res.company" name="create_missing_production_location"/>
<function model="res.company" name="create_missing_scrap_location"/>
<function model="res.company" name="create_missing_scrap_sequence"/>
</data>
</odoo>

214
data/stock_demo.xml Normal file
View File

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field eval="[(3, ref('group_stock_manager')), (4, ref('group_stock_user'))]" name="groups_id"/>
</record>
<record id="lot_product_27" model="stock.lot">
<field name="name">0000000000029</field>
<field name="product_id" ref="product.product_product_27"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="package_type_01" model="stock.package.type">
<field name="name">Pallet</field>
<field name="barcode">PAL</field>
<field name="max_weight">4000</field>
<field name="width">800</field>
<field name="height">130</field>
<field name="packaging_length">1200</field>
</record>
<record id="package_type_02" model="stock.package.type">
<field name="name">Box</field>
<field name="barcode">BOX</field>
<field name="max_weight">30</field>
<field name="width">362</field>
<field name="height">374</field>
<field name="packaging_length">562</field>
</record>
<!-- Resource: stock.quant, i.e. initial inventory -->
<record id="stock_inventory_1" model="stock.quant">
<field name="product_id" ref="product.product_product_24"/>
<field name="inventory_quantity">16.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_2" model="stock.quant">
<field name="product_id" ref="product.product_product_7"/>
<field name="inventory_quantity">18.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_3" model="stock.quant">
<field name="product_id" ref="product.product_product_6"/>
<field name="inventory_quantity">500.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_4" model="stock.quant">
<field name="product_id" ref="product.product_product_9"/>
<field name="inventory_quantity">22.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_5" model="stock.quant">
<field name="product_id" ref="product.product_product_10"/>
<field name="inventory_quantity">33.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_6" model="stock.quant">
<field name="product_id" ref="product.product_product_11"/>
<field name="inventory_quantity">26.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_6b" model="stock.quant">
<field name="product_id" ref="product.product_product_11b"/>
<field name="inventory_quantity">30.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_7" model="stock.quant">
<field name="product_id" ref="product.product_product_4"/>
<field name="inventory_quantity">45.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_7b" model="stock.quant">
<field name="product_id" ref="product.product_product_4b"/>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_7c" model="stock.quant">
<field name="product_id" ref="product.product_product_4c"/>
<field name="inventory_quantity">55.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_11" model="stock.quant">
<field name="product_id" ref="product.product_product_12"/>
<field name="inventory_quantity">10.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_12" model="stock.quant">
<field name="product_id" ref="product.product_product_13"/>
<field name="inventory_quantity">2.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_13" model="stock.quant">
<field name="product_id" ref="product.product_product_27"/>
<field name="inventory_quantity">80.0</field>
<field name="lot_id" ref="lot_product_27"/>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_14" model="stock.quant">
<field name="product_id" ref="product.product_product_3"/>
<field name="inventory_quantity">60.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_15" model="stock.quant">
<field name="product_id" ref="product.product_product_25"/>
<field name="inventory_quantity">16.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_1'),
ref('stock_inventory_2'),
ref('stock_inventory_3'),
ref('stock_inventory_4'),
ref('stock_inventory_5'),
ref('stock_inventory_6'),
ref('stock_inventory_6b'),
ref('stock_inventory_7'),
ref('stock_inventory_7b'),
ref('stock_inventory_7c'),
ref('stock_inventory_11'),
ref('stock_inventory_12'),
ref('stock_inventory_13'),
ref('stock_inventory_14'),
ref('stock_inventory_15'),
))]]" model="stock.quant" name="search"/>
</function>
<!-- Multi Company -->
<!-- Child Company 1-->
<record id="res_partner_company_1" model="res.partner">
<field name="name">My Company, Chicago</field>
<field name="is_company">1</field>
<field eval="1" name="active"/>
<field name="street">90 Streets Avenue</field>
<field model="res.country" name="country_id" search="[('code','ilike','us')]"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
<field name="zip">60610</field>
<field name="city">Chicago</field>
<field name="email">chicago@yourcompany.com</field>
<field name="phone">+1 312 349 3030</field>
<field name="website">www.example.com</field>
<field name="company_id" eval="False"/>
</record>
<record id="res_partner_address_41" model="res.partner">
<field name="name">Jeff Lawson</field>
<field name="parent_id" ref="res_partner_company_1"/>
<field name="email">jeff.lawson52@example.com</field>
<field name="phone">(461)-417-6587</field>
<field name="image_1920" type="base64" file="stock/static/img/res_partner_address_41.jpg"/>
<field name="company_id" eval="False"/>
</record>
<record id="res_company_1" model="res.company">
<field name="currency_id" ref="base.USD"/>
<field name="partner_id" ref="res_partner_company_1"/>
<field name="name">My Company (Chicago)</field>
<field name="user_ids" eval="[(4, ref('base.user_admin')), (4, ref('base.user_demo'))]"/>
</record>
<record id="base.group_multi_company" model="res.groups">
<field name="users" eval="[(4, ref('base.user_admin')), (4, ref('base.user_demo'))]"/>
</record>
<record id="base.main_company" model="res.company">
<field name="name">My Company (San Francisco)</field>
</record>
<!-- Create a ir data with the autocreated warehouse -->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'stock.stock_warehouse_shop0',
'record': obj().env['stock.warehouse'].search([('company_id', '=', obj().env.ref('stock.res_company_1').id)]),
'noupdate': True,
}]"/>
</function>
<record id="stock.stock_warehouse_shop0" model="stock.warehouse">
<field name="name">Chicago 1</field>
<field name="code">CHIC1</field>
<field name="partner_id" ref="res_partner_address_41"/>
</record>
<!-- Inventory for Chicago Warehouse -->
<record id="stock_inventory_16" model="stock.quant">
<field name="product_id" ref="product.product_product_6"/>
<field name="inventory_quantity">200.0</field>
<field name="location_id" model="stock.location"
eval="obj().env['stock.location'].search([
('company_id', '=', obj().env.ref('stock.res_company_1').id),
('location_id', '!=', False),
('child_ids', '=', False),
], limit=1)"
/>
</record>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', '=', (ref('stock_inventory_16')))]]" model="stock.quant" name="search"/>
</function>
<!-- Activate Lots options as demo data depends on it -->
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('stock.group_production_lot'))]"/>
</record>
<record id="base.group_portal" model="res.groups">
<field name="implied_ids" eval="[(4, ref('stock.group_production_lot'))]"/>
</record>
</data>
</odoo>

566
data/stock_demo2.xml Normal file
View File

@ -0,0 +1,566 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'stock.chi_picking_type_in',
'record': obj().env.ref('stock.stock_warehouse_shop0').in_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.chi_picking_type_out',
'record': obj().env.ref('stock.stock_warehouse_shop0').out_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_shop0',
'record': obj().env.ref('stock.stock_warehouse_shop0').lot_stock_id,
'noupdate': True,
}]"/>
</function>
<record id="location_refrigerator_small" model="stock.location">
<field name="name">Small Refrigerator</field>
<field name="usage">internal</field>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="barcode">WH-SHELF-REF</field>
</record>
<record id="product_cable_management_box" model="product.product">
<field name="default_code">FURN_5555</field>
<field name="name">Cable Management Box</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="categ_id" ref="product.product_category_5"/>
<field name="lst_price">100.0</field>
<field name="standard_price">70.0</field>
<field name="weight">1.0</field>
<field name="tracking">lot</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="image_1920" type="base64" file="stock/static/img/cable_management.png"/>
</record>
<record id="lot_product_cable_management" model="stock.lot">
<field name="name">LOT-000001</field>
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="lot_product_product_cable_management_box_0" model="stock.lot">
<field name="name">CM-BOX-00001</field>
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="lot_product_product_cable_management_box_1" model="stock.lot">
<field name="name">CM-BOX-00002</field>
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="stock_inventory_icecream_lot_0" model="stock.quant">
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="inventory_quantity">50.0</field>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="lot_id" ref="lot_product_product_cable_management_box_0"/>
</record>
<record id="stock_inventory_icecream_lot_1" model="stock.quant">
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="inventory_quantity">40.0</field>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="lot_id" ref="lot_product_product_cable_management_box_1"/>
</record>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_icecream_lot_0'),
ref('stock_inventory_icecream_lot_1')
))]]" model="stock.quant" name="search"/>
</function>
<!-- Create STOCK_MOVE for OUT -->
<record id="outgoing_shipment_main_warehouse" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_27').name,
'product_id': ref('product.product_product_27'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 15.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse1" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=15)"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_6').name,
'product_id': ref('product.product_product_6'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 180.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse2" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=5)"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_02').name,
'product_id': ref('product.product_delivery_02'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 45.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse3" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="user_id"></field>
<field name="origin">your company warehouse</field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_5').name,
'product_id': ref('product.product_product_5'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 75.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse4" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=7)"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_3').name,
'product_id': ref('product.product_product_3'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 16.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse5" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=12)"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_3').name,
'product_id': ref('product.product_product_3'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 40.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<record id="outgoing_shipment_main_warehouse6" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
<field name="origin">outgoing shipment</field>
<field name="user_id"></field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=20)"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_6').name,
'product_id': ref('product.product_product_6'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 50.0,
'picking_type_id': ref('stock.picking_type_out'),
'location_id': ref('stock.stock_location_stock'),
'location_dest_id': ref('stock.stock_location_customers'),
})]"/>
</record>
<!-- Create STOCK_PICKING for IN -->
<record id="incomming_shipment" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.picking_type_in"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('stock.product_cable_management_box').name,
'product_id': ref('stock.product_cable_management_box'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 50.0,
'picking_type_id': ref('stock.picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_stock'),
})]"/>
</record>
<record id="incomming_shipment1" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.picking_type_in"/>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=5)"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_01').name,
'product_id': ref('product.product_delivery_01'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 35.0,
'picking_type_id': ref('stock.picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_stock'),
})]"/>
</record>
<record id="incomming_shipment2" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.picking_type_in"/>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_22').name,
'product_id': ref('product.product_product_22'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 125.0,
'picking_type_id': ref('stock.picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_stock'),
})]"/>
</record>
<record id="incomming_shipment3" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.picking_type_in"/>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_10').name,
'product_id': ref('product.product_product_10'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 120.0,
'picking_type_id': ref('stock.picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_stock'),
})]"/>
</record>
<record id="incomming_shipment4" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.picking_type_in"/>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="state">draft</field>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_02').name,
'product_id': ref('product.product_delivery_02'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 50.0,
'picking_type_id': ref('stock.picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_stock'),
})]"/>
</record>
<!-- Create STOCK_PICKING_IN for Chicago Warehouse-->
<record id="incomming_chicago_warehouse" model="stock.picking">
<field name="picking_type_id" ref="stock.chi_picking_type_in"/>
<field name="origin">incoming_chicago_warehouse</field>
<field name="user_id"></field>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_shop0"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_9').name,
'product_id': ref('product.product_product_9'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 25.0,
'picking_type_id': ref('stock.chi_picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_shop0'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="incomming_chicago_warehouse1" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_in"/>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=5)"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_shop0"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_01').name,
'product_id': ref('product.product_delivery_01'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 180.0,
'picking_type_id': ref('stock.chi_picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_shop0'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="incomming_chicago_warehouse2" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_in"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_shop0"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_01').name,
'product_id': ref('product.product_delivery_01'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 45.0,
'picking_type_id': ref('stock.chi_picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_shop0'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="incomming_chicago_warehouse3" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_in"/>
<field name="origin">chicago_warehouse</field>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=2)"/>
<field name="location_id" ref="stock.stock_location_suppliers"/>
<field name="location_dest_id" ref="stock.stock_location_shop0"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_22').name,
'product_id': ref('product.product_product_22'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 75.0,
'picking_type_id': ref('stock.chi_picking_type_in'),
'location_id': ref('stock.stock_location_suppliers'),
'location_dest_id': ref('stock.stock_location_shop0'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<!-- Create STOCK_PICKING_OUT for Chicago Warehouse -->
<record id="outgoing_chicago_warehouse" model="stock.picking">
<field name="picking_type_id" ref="stock.chi_picking_type_out"/>
<field name="user_id"></field>
<field name="origin">outgoing_chicago_warehouse</field>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_shop0"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_27').name,
'product_id': ref('product.product_product_27'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 15.0,
'picking_type_id': ref('stock.chi_picking_type_out'),
'location_id': ref('stock.stock_location_shop0'),
'location_dest_id': ref('stock.stock_location_customers'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="outgoing_chicago_warehouse1" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_out"/>
<field name="origin">outgoing_shipment_chicago_warehouse</field>
<field name="scheduled_date" eval="DateTime.today() - timedelta(days=10)"/>
<field name="location_id" ref="stock.stock_location_shop0"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_6').name,
'product_id': ref('product.product_product_6'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 180.0,
'picking_type_id': ref('stock.chi_picking_type_out'),
'location_id': ref('stock.stock_location_shop0'),
'location_dest_id': ref('stock.stock_location_customers'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="outgoing_chicago_warehouse2" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_out"/>
<field name="origin">chicago_warehouse</field>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_shop0"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_delivery_02').name,
'product_id': ref('product.product_delivery_02'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 45.0,
'picking_type_id': ref('stock.chi_picking_type_out'),
'location_id': ref('stock.stock_location_shop0'),
'location_dest_id': ref('stock.stock_location_customers'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<record id="outgoing_chicago_warehouse3" model="stock.picking">
<field name="user_id"></field>
<field name="picking_type_id" ref="stock.chi_picking_type_out"/>
<field name="origin">outgoing chicago warehouse</field>
<field name="scheduled_date" eval="DateTime.today()"/>
<field name="location_id" ref="stock.stock_location_shop0"/>
<field name="location_dest_id" ref="stock.stock_location_customers"/>
<field name="state">draft</field>
<field name="company_id" ref="stock.res_company_1"/>
<field name="move_ids" model="stock.move" eval="[(0, 0, {
'name': obj().env.ref('product.product_product_5').name,
'product_id': ref('product.product_product_5'),
'product_uom': ref('uom.product_uom_unit'),
'product_uom_qty': 75.0,
'picking_type_id': ref('stock.chi_picking_type_out'),
'location_id': ref('stock.stock_location_shop0'),
'location_dest_id': ref('stock.stock_location_customers'),
'company_id': ref('stock.res_company_1'),
})]"/>
</record>
<function model="stock.picking" name="action_confirm">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse1').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse4').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse5').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse6').id,
obj().env.ref('stock.incomming_shipment1').id,
obj().env.ref('stock.incomming_shipment2').id,
obj().env.ref('stock.incomming_shipment3').id,
obj().env.ref('stock.outgoing_chicago_warehouse1').id,
obj().env.ref('stock.outgoing_chicago_warehouse2').id,
obj().env.ref('stock.outgoing_chicago_warehouse3').id,
obj().env.ref('stock.incomming_chicago_warehouse1').id,
obj().env.ref('stock.incomming_chicago_warehouse2').id,
obj().env.ref('stock.incomming_chicago_warehouse3').id]"/>
</function>
<function model="stock.picking" name="action_assign">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse1').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse5').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse6').id,
obj().env.ref('stock.outgoing_chicago_warehouse1').id,
]"/>
</function>
<!-- Adds move lines qty. done -->
<function model="stock.move.line" name="write">
<value model="stock.move.line" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse1'))]"/>
<value eval="{'quantity': 100}"/>
</function>
<function model="stock.move.line" name="write">
<value model="stock.move.line" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse5'))]"/>
<value eval="{'quantity': 32}"/>
</function>
<function model="stock.move.line" name="write">
<value model="stock.move.line" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse6'))]"/>
<value eval="{'quantity': 50}"/>
</function>
<function model="stock.move.line" name="write">
<value model="stock.move.line" search="[('picking_id', '=', ref('stock.incomming_chicago_warehouse1'))]"/>
<value eval="{'quantity': 100}"/>
</function>
<function model="stock.move.line" name="write">
<value model="stock.move.line" search="[('picking_id', '=', ref('stock.outgoing_chicago_warehouse1'))]"/>
<value eval="{'quantity': 100}"/>
</function>
<function model="stock.picking" name="_action_done">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse1').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse5').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse6').id,
obj().env.ref('stock.incomming_chicago_warehouse1').id,
obj().env.ref('stock.outgoing_chicago_warehouse1').id,
obj().env.ref('stock.outgoing_chicago_warehouse2').id]"/>
</function>
<record id="stock.outgoing_chicago_warehouse1" model="stock.picking">
<field name="date_done" eval="DateTime.today() - timedelta(days=5)"/>
</record>
<record id="stock.outgoing_shipment_main_warehouse5" model="stock.picking">
<field name="date_done" eval="DateTime.today() - timedelta(days=17)"/>
</record>
<record id="stock.outgoing_shipment_main_warehouse6" model="stock.picking">
<field name="date_done" eval="DateTime.today() - timedelta(days=7)"/>
</record>
<function model="stock.move" name="write">
<value model="stock.move" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse4'))]"/>
<value eval="{'date': DateTime.today() + timedelta(days=3)}"/>
</function>
<function model="stock.move" name="write">
<value model="stock.move" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse5'))]"/>
<value eval="{'date': DateTime.today() - timedelta(days=18)}"/>
</function>
<function model="stock.move" name="write">
<value model="stock.move" search="[('picking_id', '=', ref('stock.outgoing_shipment_main_warehouse6'))]"/>
<value eval="{'date': DateTime.today() - timedelta(days=7)}"/>
</function>
</data>
</odoo>

132
data/stock_demo_pre.xml Normal file
View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="stock_location_14" model="stock.location">
<field name="name">Shelf 2</field>
<field name="posx">0</field>
<field name="barcode">2601985</field>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_location_components" model="stock.location">
<field name="name">Shelf 1</field>
<field name="posx">0</field>
<field name="barcode">2601892</field>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="location_order" model="stock.location">
<field name="name">Order Processing</field>
<field name="usage">internal</field>
<field name="location_id" ref="stock.stock_location_company"/>
</record>
<record id="location_dispatch_zone" model="stock.location">
<field name="name">Dispatch Zone</field>
<field name="usage">internal</field>
<field name="location_id" ref="stock.location_order"/>
</record>
<record id="location_gate_a" model="stock.location">
<field name="name">Gate A</field>
<field name="usage">internal</field>
<field name="location_id" ref="stock.location_dispatch_zone"/>
</record>
<record id="location_gate_b" model="stock.location">
<field name="name">Gate B</field>
<field name="usage">internal</field>
<field name="location_id" ref="stock.location_dispatch_zone"/>
</record>
<record id="product.product_product_3" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_4" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_5" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_6" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_7" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_8" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_9" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_10" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_11" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_12" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_13" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_16" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_20" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_22" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_24" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_25" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_product_27" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_delivery_01" model="product.product">
<field name="type">product</field>
</record>
<record id="product.consu_delivery_03" model="product.product">
<field name="type">product</field>
</record>
<record id="product.consu_delivery_02" model="product.product">
<field name="type">product</field>
</record>
<record id="product.consu_delivery_01" model="product.product">
<field name="type">product</field>
</record>
<record id="product.product_order_01" model="product.product">
<field name="type">product</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,44 @@
<odoo>
<data noupdate="1">
<!-- Resource: stock.warehouse.orderpoint -->
<record id="stock_warehouse_orderpoint_1" model="stock.warehouse.orderpoint">
<field name="product_max_qty">10.0</field>
<field name="product_min_qty">5.0</field>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field model="stock.warehouse" name="warehouse_id" search="[]"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_warehouse_orderpoint_2" model="stock.warehouse.orderpoint">
<field name="product_max_qty">12.0</field>
<field name="product_min_qty">5.0</field>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field model="stock.warehouse" name="warehouse_id" search="[]"/>
<field name="product_id" ref="product.product_product_20"/>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_warehouse_orderpoint_5" model="stock.warehouse.orderpoint">
<field name="product_max_qty">5.0</field>
<field name="product_min_qty">3.0</field>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field model="stock.warehouse" name="warehouse_id" search="[]"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_warehouse_orderpoint_shop1_cpu1" model="stock.warehouse.orderpoint">
<field name="product_max_qty">20.0</field>
<field name="product_min_qty">10.0</field>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="company_id" ref="stock.res_company_1"/>
<field name="warehouse_id" ref="stock.stock_warehouse_shop0"/>
<field name="location_id" ref="stock.stock_location_shop0"/>
<field name="product_id" ref="product.product_product_9"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sequences for orderpoint -->
<record id="sequence_mrp_op" model="ir.sequence">
<field name="name">Stock orderpoint</field>
<field name="code">stock.orderpoint</field>
<field name="prefix">OP/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="company_id"></field>
</record>
<!-- Sequences for procurement group -->
<record id="sequence_proc_group" model="ir.sequence">
<field name="name">Procurement Group</field>
<field name="code">procurement.group</field>
<field name="prefix">PG/</field>
<field name="padding">6</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
<!-- Sequences for pickings -->
<record id="seq_picking_internal" model="ir.sequence">
<field name="name">Picking INT</field>
<field name="code">stock.picking</field>
<field name="prefix">INT/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<!-- Sequences from tracking numbers -->
<record id="sequence_production_lots" model="ir.sequence">
<field name="name">Serial Numbers</field>
<field name="code">stock.lot.serial</field>
<field name="prefix"></field>
<field name="padding">7</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="company_id" eval="False"/>
</record>
<!-- Sequences for stock.quant.package -->
<record id="seq_quant_package" model="ir.sequence">
<field name="name">Packages</field>
<field name="code">stock.quant.package</field>
<field name="prefix">PACK</field>
<field name="padding">7</field>
<field name="company_id" eval="False"/>
</record>
<!-- Scheduler -->
<record forcecreate="True" id="ir_cron_scheduler_action" model="ir.cron">
<field name="name">Procurement: run scheduler</field>
<field name="model_id" ref="model_procurement_group"/>
<field name="state">code</field>
<field name="code">
model.run_scheduler(True)
</field>
<field eval="True" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="stock_storage_category_high_small" model="stock.storage.category">
<field name="name">High frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_high_big" model="stock.storage.category">
<field name="name">High frequency - Big</field>
<field name="max_weight">3000</field>
</record>
<record id="stock_storage_category_medium_small" model="stock.storage.category">
<field name="name">Medium frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_medium_big" model="stock.storage.category">
<field name="name">Medium frequency - Big</field>
<field name="max_weight">3000</field>
</record>
<record id="stock_storage_category_low_small" model="stock.storage.category">
<field name="name">Low frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_low_big" model="stock.storage.category">
<field name="name">Low frequency - Big</field>
<field name="max_weight">3000</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_stock_report" model="ir.actions.client">
<field name="name">Traceability Report</field>
<field name="tag">stock_report_generic</field>
<field name="context" eval="{'url': '/stock/output_format/stock?active_id=:active_id&amp;active_model=:active_model', 'model': 'stock.traceability.report'}" />
</record>
</odoo>

9547
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

11163
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

9595
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

10874
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

9549
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

11240
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

11181
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

11020
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

11406
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

9570
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

9546
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

11364
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

11374
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

9553
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

9546
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

9562
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

9545
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

9571
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

9564
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

9548
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

9545
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

11178
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

11027
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

11275
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

11377
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

9544
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

9545
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

9557
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

10884
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

9615
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

10854
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

11288
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

9552
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

11350
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

10881
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

9545
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

9549
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

10940
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

9547
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

10905
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

10705
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

9550
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

9663
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

9585
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

11354
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

11211
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

10855
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

11343
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

9693
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

11345
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

10891
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

10758
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

11175
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

9549
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

10660
i18n/stock.pot Normal file

File diff suppressed because it is too large Load Diff

11224
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

11173
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

11150
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

11222
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

11277
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

10889
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

10871
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

23
models/__init__.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import barcode
from . import ir_actions_report
from . import product_strategy
from . import res_company
from . import res_partner
from . import res_config_settings
from . import stock_location
from . import stock_move
from . import stock_move_line
from . import stock_orderpoint
from . import stock_lot
from . import stock_picking
from . import stock_quant
from . import stock_rule
from . import stock_warehouse
from . import stock_scrap
from . import product
from . import stock_package_level
from . import stock_package_type
from . import stock_storage_category

20
models/barcode.py Normal file
View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class BarcodeRule(models.Model):
_inherit = 'barcode.rule'
type = fields.Selection(selection_add=[
('weight', 'Weighted Product'),
('location', 'Location'),
('lot', 'Lot'),
('package', 'Package')
], ondelete={
'weight': 'set default',
'location': 'set default',
'lot': 'set default',
'package': 'set default',
})

View File

@ -0,0 +1,16 @@
from odoo import models
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
def _get_rendering_context(self, report, docids, data):
data = super()._get_rendering_context(report, docids, data)
if report.report_name == 'stock.report_reception_report_label' and not docids:
docids = data['docids']
docs = self.env[report.model].browse(docids)
data.update({
'doc_ids': docids,
'docs': docs,
})
return data

1102
models/product.py Normal file

File diff suppressed because it is too large Load Diff

146
models/product_strategy.py Normal file
View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
class RemovalStrategy(models.Model):
_name = 'product.removal'
_description = 'Removal Strategy'
name = fields.Char('Name', required=True, translate=True)
method = fields.Char("Method", required=True, translate=True, help="FIFO, LIFO...")
class StockPutawayRule(models.Model):
_name = 'stock.putaway.rule'
_order = 'sequence,product_id'
_description = 'Putaway Rule'
_check_company_auto = True
def _default_category_id(self):
if self.env.context.get('active_model') == 'product.category':
return self.env.context.get('active_id')
def _default_location_id(self):
if self.env.context.get('active_model') == 'stock.location':
return self.env.context.get('active_id')
if not self.env.user.has_group('stock.group_stock_multi_warehouses'):
wh = self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1)
input_loc, _ = wh._get_input_output_locations(wh.reception_steps, wh.delivery_steps)
return input_loc
def _default_product_id(self):
if self.env.context.get('active_model') == 'product.template' and self.env.context.get('active_id'):
product_template = self.env['product.template'].browse(self.env.context.get('active_id'))
product_template = product_template.exists()
if product_template.product_variant_count == 1:
return product_template.product_variant_id
elif self.env.context.get('active_model') == 'product.product':
return self.env.context.get('active_id')
product_id = fields.Many2one(
'product.product', 'Product', check_company=True,
default=_default_product_id,
domain="[('product_tmpl_id', '=', context.get('active_id', False))] if context.get('active_model') == 'product.template' else [('type', '!=', 'service')]",
ondelete='cascade')
category_id = fields.Many2one('product.category', 'Product Category',
default=_default_category_id, domain=[('filter_for_stock_putaway_rule', '=', True)], ondelete='cascade')
location_in_id = fields.Many2one(
'stock.location', 'When product arrives in', check_company=True,
domain="[('child_ids', '!=', False)]",
default=_default_location_id, required=True, ondelete='cascade', index=True)
location_out_id = fields.Many2one(
'stock.location', 'Store to sublocation', check_company=True,
domain="[('id', 'child_of', location_in_id)]",
required=True, ondelete='cascade')
sequence = fields.Integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list.")
company_id = fields.Many2one(
'res.company', 'Company', required=True,
default=lambda s: s.env.company.id, index=True)
package_type_ids = fields.Many2many('stock.package.type', string='Package Type', check_company=True)
storage_category_id = fields.Many2one('stock.storage.category', 'Storage Category', ondelete='cascade', check_company=True)
active = fields.Boolean('Active', default=True)
@api.onchange('location_in_id')
def _onchange_location_in(self):
child_location_count = 0
if self.location_out_id:
child_location_count = self.env['stock.location'].search_count([
('id', '=', self.location_out_id.id),
('id', 'child_of', self.location_in_id.id),
('id', '!=', self.location_in_id.id),
])
if not child_location_count or not self.location_out_id:
self.location_out_id = self.location_in_id
@api.model_create_multi
def create(self, vals_list):
rules = super().create(vals_list)
rules._enable_show_reserved()
return rules
def write(self, vals):
if 'company_id' in vals:
for rule in self:
if rule.company_id.id != vals['company_id']:
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
self._enable_show_reserved()
return super(StockPutawayRule, self).write(vals)
def _enable_show_reserved(self):
out_locations = self.location_out_id
if out_locations:
self.env['stock.picking.type'].with_context(active_test=False)\
.search([('default_location_dest_id', 'in', out_locations.ids), ('show_reserved', '=', False)])\
.write({'show_reserved': True})
def _get_putaway_location(self, product, quantity=0, package=None, packaging=None, qty_by_location=None):
# find package type on package or packaging
package_type = self.env['stock.package.type']
if package:
package_type = package.package_type_id
elif packaging:
package_type = packaging.package_type_id
checked_locations = set()
for putaway_rule in self:
location_out = putaway_rule.location_out_id
child_locations = location_out.child_internal_location_ids
if not putaway_rule.storage_category_id:
if location_out in checked_locations:
continue
if location_out._check_can_be_used(product, quantity, package, qty_by_location[location_out.id]):
return location_out
continue
else:
child_locations = child_locations.filtered(lambda loc: loc.storage_category_id == putaway_rule.storage_category_id)
# check if already have the product/package type stored
for location in child_locations:
if location in checked_locations:
continue
if package_type:
if location.quant_ids.filtered(lambda q: q.package_id and q.package_id.package_type_id == package_type):
if location._check_can_be_used(product, quantity, package=package, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
elif float_compare(qty_by_location[location.id], 0, precision_rounding=product.uom_id.rounding) > 0:
if location._check_can_be_used(product, quantity, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
# check locations with matched storage category
for location in child_locations.filtered(lambda l: l.storage_category_id == putaway_rule.storage_category_id):
if location in checked_locations:
continue
if location._check_can_be_used(product, quantity, package, qty_by_location[location.id]):
return location
checked_locations.add(location)
return None

208
models/res_company.py Normal file
View File

@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class Company(models.Model):
_inherit = "res.company"
_check_company_auto = True
def _default_confirmation_mail_template(self):
try:
return self.env.ref('stock.mail_template_data_delivery_confirmation').id
except ValueError:
return False
# used for resupply routes between warehouses that belong to this company
internal_transit_location_id = fields.Many2one(
'stock.location', 'Internal Transit Location', ondelete="restrict", check_company=True)
stock_move_email_validation = fields.Boolean("Email Confirmation picking", default=False)
stock_mail_confirmation_template_id = fields.Many2one('mail.template', string="Email Template confirmation picking",
domain="[('model', '=', 'stock.picking')]",
default=_default_confirmation_mail_template,
help="Email sent to the customer once the order is done.")
annual_inventory_month = fields.Selection([
('1', 'January'),
('2', 'February'),
('3', 'March'),
('4', 'April'),
('5', 'May'),
('6', 'June'),
('7', 'July'),
('8', 'August'),
('9', 'September'),
('10', 'October'),
('11', 'November'),
('12', 'December'),
], string='Annual Inventory Month',
default='12',
help="Annual inventory month for products not in a location with a cyclic inventory date. Set to no month if no automatic annual inventory.")
annual_inventory_day = fields.Integer(
string='Day of the month', default=31,
help="""Day of the month when the annual inventory should occur. If zero or negative, then the first day of the month will be selected instead.
If greater than the last day of a month, then the last day of the month will be selected instead.""")
def _create_transit_location(self):
'''Create a transit location with company_id being the given company_id. This is needed
in case of resuply routes between warehouses belonging to the same company, because
we don't want to create accounting entries at that time.
'''
parent_location = self.env.ref('stock.stock_location_locations', raise_if_not_found=False)
for company in self:
location = self.env['stock.location'].create({
'name': _('Inter-warehouse transit'),
'usage': 'transit',
'location_id': parent_location and parent_location.id or False,
'company_id': company.id,
'active': False
})
company.write({'internal_transit_location_id': location.id})
company.partner_id.with_company(company).write({
'property_stock_customer': location.id,
'property_stock_supplier': location.id,
})
def _create_inventory_loss_location(self):
parent_location = self.env.ref('stock.stock_location_locations_virtual', raise_if_not_found=False)
for company in self:
inventory_loss_location = self.env['stock.location'].create({
'name': 'Inventory adjustment',
'usage': 'inventory',
'location_id': parent_location.id,
'company_id': company.id,
})
self.env['ir.property']._set_default(
"property_stock_inventory",
"product.template",
inventory_loss_location,
company.id,
)
def _create_production_location(self):
parent_location = self.env.ref('stock.stock_location_locations_virtual', raise_if_not_found=False)
for company in self:
production_location = self.env['stock.location'].create({
'name': 'Production',
'usage': 'production',
'location_id': parent_location.id,
'company_id': company.id,
})
self.env['ir.property']._set_default(
"property_stock_production",
"product.template",
production_location,
company.id,
)
def _create_scrap_location(self):
parent_location = self.env.ref('stock.stock_location_locations_virtual', raise_if_not_found=False)
for company in self:
scrap_location = self.env['stock.location'].create({
'name': 'Scrap',
'usage': 'inventory',
'location_id': parent_location.id,
'company_id': company.id,
'scrap_location': True,
})
def _create_scrap_sequence(self):
scrap_vals = []
for company in self:
scrap_vals.append({
'name': '%s Sequence scrap' % company.name,
'code': 'stock.scrap',
'company_id': company.id,
'prefix': 'SP/',
'padding': 5,
'number_next': 1,
'number_increment': 1
})
if scrap_vals:
self.env['ir.sequence'].create(scrap_vals)
@api.model
def create_missing_warehouse(self):
""" This hook is used to add a warehouse on existing companies
when module stock is installed.
"""
company_ids = self.env['res.company'].search([])
company_with_warehouse = self.env['stock.warehouse'].with_context(active_test=False).search([]).mapped('company_id')
company_without_warehouse = company_ids - company_with_warehouse
for company in company_without_warehouse:
self.env['stock.warehouse'].create({
'name': company.name,
'code': company.name[:5],
'company_id': company.id,
'partner_id': company.partner_id.id,
})
@api.model
def create_missing_transit_location(self):
company_without_transit = self.env['res.company'].search([('internal_transit_location_id', '=', False)])
company_without_transit._create_transit_location()
@api.model
def create_missing_inventory_loss_location(self):
company_ids = self.env['res.company'].search([])
inventory_loss_product_template_field = self.env['ir.model.fields']._get('product.template', 'property_stock_inventory')
companies_having_property = self.env['ir.property'].sudo().search([('fields_id', '=', inventory_loss_product_template_field.id), ('res_id', '=', False)]).mapped('company_id')
company_without_property = company_ids - companies_having_property
company_without_property._create_inventory_loss_location()
@api.model
def create_missing_production_location(self):
company_ids = self.env['res.company'].search([])
production_product_template_field = self.env['ir.model.fields']._get('product.template', 'property_stock_production')
companies_having_property = self.env['ir.property'].sudo().search([('fields_id', '=', production_product_template_field.id), ('res_id', '=', False)]).mapped('company_id')
company_without_property = company_ids - companies_having_property
company_without_property._create_production_location()
@api.model
def create_missing_scrap_location(self):
company_ids = self.env['res.company'].search([])
companies_having_scrap_loc = self.env['stock.location'].search([('scrap_location', '=', True)]).mapped('company_id')
company_without_property = company_ids - companies_having_scrap_loc
company_without_property._create_scrap_location()
@api.model
def create_missing_scrap_sequence(self):
company_ids = self.env['res.company'].search([])
company_has_scrap_seq = self.env['ir.sequence'].search([('code', '=', 'stock.scrap')]).mapped('company_id')
company_todo_sequence = company_ids - company_has_scrap_seq
company_todo_sequence._create_scrap_sequence()
def _create_per_company_locations(self):
self.ensure_one()
self._create_transit_location()
self._create_inventory_loss_location()
self._create_production_location()
self._create_scrap_location()
def _create_per_company_sequences(self):
self.ensure_one()
self._create_scrap_sequence()
def _create_per_company_picking_types(self):
self.ensure_one()
def _create_per_company_rules(self):
self.ensure_one()
@api.model_create_multi
def create(self, vals_list):
companies = super().create(vals_list)
for company in companies:
company.sudo()._create_per_company_locations()
company.sudo()._create_per_company_sequences()
company.sudo()._create_per_company_picking_types()
company.sudo()._create_per_company_rules()
self.env['stock.warehouse'].sudo().create([{
'name': company.name,
'code': self.env.context.get('default_code') or company.name[:5],
'company_id': company.id,
'partner_id': company.partner_id.id
} for company in companies])
return companies

View File

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_product_expiry = fields.Boolean("Expiration Dates",
help="Track following dates on lots & serial numbers: best before, removal, end of life, alert. \n Such dates are set automatically at lot/serial number creation based on values set on the product (in days).")
group_stock_production_lot = fields.Boolean("Lots & Serial Numbers",
implied_group='stock.group_production_lot', group="base.group_user,base.group_portal")
group_stock_lot_print_gs1 = fields.Boolean("Print GS1 Barcodes for Lots & Serial Numbers",
implied_group='stock.group_stock_lot_print_gs1')
group_lot_on_delivery_slip = fields.Boolean("Display Lots & Serial Numbers on Delivery Slips",
implied_group='stock.group_lot_on_delivery_slip', group="base.group_user,base.group_portal")
group_stock_tracking_lot = fields.Boolean("Packages",
implied_group='stock.group_tracking_lot')
group_stock_tracking_owner = fields.Boolean("Consignment",
implied_group='stock.group_tracking_owner')
group_stock_adv_location = fields.Boolean("Multi-Step Routes",
implied_group='stock.group_adv_location',
help="Add and customize route operations to process product moves in your warehouse(s): e.g. unload > quality control > stock for incoming products, pick > pack > ship for outgoing products. \n You can also set putaway strategies on warehouse locations in order to send incoming products into specific child locations straight away (e.g. specific bins, racks).")
group_warning_stock = fields.Boolean("Warnings for Stock", implied_group='stock.group_warning_stock')
group_stock_sign_delivery = fields.Boolean("Signature", implied_group='stock.group_stock_sign_delivery')
module_stock_picking_batch = fields.Boolean("Batch Transfers")
group_stock_picking_wave = fields.Boolean('Wave Transfers', implied_group='stock.group_stock_picking_wave',
help="Group your move operations in wave transfer to process them together")
module_stock_barcode = fields.Boolean("Barcode Scanner")
stock_move_email_validation = fields.Boolean(related='company_id.stock_move_email_validation', readonly=False)
module_stock_sms = fields.Boolean("SMS Confirmation")
module_delivery = fields.Boolean("Delivery Methods")
module_delivery_dhl = fields.Boolean("DHL Express Connector")
module_delivery_fedex = fields.Boolean("FedEx Connector")
module_delivery_ups = fields.Boolean("UPS Connector")
module_delivery_usps = fields.Boolean("USPS Connector")
module_delivery_bpost = fields.Boolean("bpost Connector")
module_delivery_easypost = fields.Boolean("Easypost Connector")
module_delivery_sendcloud = fields.Boolean("Sendcloud Connector")
module_delivery_shiprocket = fields.Boolean("Shiprocket Connector")
module_quality_control = fields.Boolean("Quality")
module_quality_control_worksheet = fields.Boolean("Quality Worksheet")
group_stock_multi_locations = fields.Boolean('Storage Locations', implied_group='stock.group_stock_multi_locations',
help="Store products in specific locations of your warehouse (e.g. bins, racks) and to track inventory accordingly.")
group_stock_storage_categories = fields.Boolean(
'Storage Categories', implied_group='stock.group_stock_storage_categories')
annual_inventory_month = fields.Selection(related='company_id.annual_inventory_month', readonly=False)
annual_inventory_day = fields.Integer(related='company_id.annual_inventory_day', readonly=False)
group_stock_reception_report = fields.Boolean("Reception Report", implied_group='stock.group_reception_report')
module_stock_dropshipping = fields.Boolean("Dropshipping")
@api.onchange('group_stock_multi_locations')
def _onchange_group_stock_multi_locations(self):
if not self.group_stock_multi_locations:
self.group_stock_adv_location = False
self.group_stock_storage_categories = False
@api.onchange('group_stock_production_lot')
def _onchange_group_stock_production_lot(self):
if not self.group_stock_production_lot:
self.group_lot_on_delivery_slip = False
self.module_product_expiry = False
@api.onchange('group_stock_adv_location')
def onchange_adv_location(self):
if self.group_stock_adv_location and not self.group_stock_multi_locations:
self.group_stock_multi_locations = True
def set_values(self):
warehouse_grp = self.env.ref('stock.group_stock_multi_warehouses')
location_grp = self.env.ref('stock.group_stock_multi_locations')
base_user = self.env.ref('base.group_user')
base_user_implied_ids = base_user.implied_ids
if not self.group_stock_multi_locations and location_grp in base_user_implied_ids and warehouse_grp in base_user_implied_ids:
raise UserError(_("You can't deactivate the multi-location if you have more than once warehouse by company"))
# Deactivate putaway rules with storage category when not in storage category
# group. Otherwise, active them.
storage_cate_grp = self.env.ref('stock.group_stock_storage_categories')
PutawayRule = self.env['stock.putaway.rule']
if self.group_stock_storage_categories and storage_cate_grp not in base_user_implied_ids:
putaway_rules = PutawayRule.search([
('active', '=', False),
('storage_category_id', '!=', False)
])
if putaway_rules:
putaway_rules.active = True
elif not self.group_stock_storage_categories and storage_cate_grp in base_user_implied_ids:
putaway_rules = PutawayRule.search([('storage_category_id', '!=', False)])
if putaway_rules:
putaway_rules.active = False
previous_group = self.default_get(['group_stock_multi_locations', 'group_stock_production_lot', 'group_stock_tracking_lot'])
super().set_values()
if not self.user_has_groups('stock.group_stock_manager'):
return
# If we just enabled multiple locations with this settings change, we can deactivate
# the internal operation types of the warehouses, so they won't appear in the dashboard.
# Otherwise (if we just disabled multiple locations with this settings change), activate them
warehouse_obj = self.env['stock.warehouse']
if self.group_stock_multi_locations and not previous_group.get('group_stock_multi_locations'):
# override active_test that is false in set_values
warehouse_obj.with_context(active_test=True).search([]).int_type_id.active = True
# Disable the views removing the create button from the location list and form.
# Be resilient if the views have been deleted manually.
for view in (
self.env.ref('stock.stock_location_view_tree2_editable', raise_if_not_found=False),
self.env.ref('stock.stock_location_view_form_editable', raise_if_not_found=False),
):
if view:
view.active = False
elif not self.group_stock_multi_locations and previous_group.get('group_stock_multi_locations'):
warehouse_obj.search([
('reception_steps', '=', 'one_step'),
('delivery_steps', '=', 'ship_only')
]).int_type_id.active = False
# Enable the views removing the create button from the location list and form.
# Be resilient if the views have been deleted manually.
for view in (
self.env.ref('stock.stock_location_view_tree2_editable', raise_if_not_found=False),
self.env.ref('stock.stock_location_view_form_editable', raise_if_not_found=False),
):
if view:
view.active = True
if not self.group_stock_production_lot and previous_group.get('group_stock_production_lot'):
if self.env['product.product'].search_count([('tracking', '!=', 'none')], limit=1):
raise UserError(_("You have product(s) in stock that have lot/serial number tracking enabled. \nSwitch off tracking on all the products before switching off this setting."))
return

21
models/res_partner.py Normal file
View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.addons.base.models.res_partner import WARNING_HELP, WARNING_MESSAGE
class Partner(models.Model):
_inherit = 'res.partner'
_check_company_auto = True
property_stock_customer = fields.Many2one(
'stock.location', string="Customer Location", company_dependent=True, check_company=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', allowed_company_ids[0])]",
help="The stock location used as destination when sending goods to this contact.")
property_stock_supplier = fields.Many2one(
'stock.location', string="Vendor Location", company_dependent=True, check_company=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', allowed_company_ids[0])]",
help="The stock location used as source when receiving goods from this contact.")
picking_warn = fields.Selection(WARNING_MESSAGE, 'Stock Picking', help=WARNING_HELP, default='no-message')
picking_warn_msg = fields.Text('Message for Stock Picking')

474
models/stock_location.py Normal file
View File

@ -0,0 +1,474 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from collections import defaultdict, OrderedDict
from datetime import timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools.float_utils import float_compare
class Location(models.Model):
_name = "stock.location"
_description = "Inventory Locations"
_parent_name = "location_id"
_parent_store = True
_order = 'complete_name, id'
_rec_name = 'complete_name'
_rec_names_search = ['complete_name', 'barcode']
_check_company_auto = True
@api.model
def default_get(self, fields):
res = super(Location, self).default_get(fields)
if 'barcode' in fields and 'barcode' not in res and res.get('complete_name'):
res['barcode'] = res['complete_name']
return res
name = fields.Char('Location Name', required=True)
complete_name = fields.Char("Full Location Name", compute='_compute_complete_name', recursive=True, store=True)
active = fields.Boolean('Active', default=True, help="By unchecking the active field, you may hide a location without deleting it.")
usage = fields.Selection([
('supplier', 'Vendor Location'),
('view', 'View'),
('internal', 'Internal Location'),
('customer', 'Customer Location'),
('inventory', 'Inventory Loss'),
('production', 'Production'),
('transit', 'Transit Location')], string='Location Type',
default='internal', index=True, required=True,
help="* Vendor Location: Virtual location representing the source location for products coming from your vendors"
"\n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products"
"\n* Internal Location: Physical locations inside your own warehouses,"
"\n* Customer Location: Virtual location representing the destination location for products sent to your customers"
"\n* Inventory Loss: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)"
"\n* Production: Virtual counterpart location for production operations: this location consumes the components and produces finished products"
"\n* Transit Location: Counterpart location that should be used in inter-company or inter-warehouses operations")
location_id = fields.Many2one(
'stock.location', 'Parent Location', index=True, ondelete='cascade', check_company=True,
help="The parent location that includes this location. Example : The 'Dispatch Zone' is the 'Gate 1' parent location.")
child_ids = fields.One2many('stock.location', 'location_id', 'Contains')
child_internal_location_ids = fields.Many2many(
'stock.location',
string='Internal locations among descendants',
compute='_compute_child_internal_location_ids',
recursive=True,
help='This location (if it\'s internal) and all its descendants filtered by type=Internal.'
)
comment = fields.Html('Additional Information')
posx = fields.Integer('Corridor (X)', default=0, help="Optional localization details, for information purpose only")
posy = fields.Integer('Shelves (Y)', default=0, help="Optional localization details, for information purpose only")
posz = fields.Integer('Height (Z)', default=0, help="Optional localization details, for information purpose only")
parent_path = fields.Char(index=True, unaccent=False)
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company, index=True,
help='Let this field empty if this location is shared between companies')
scrap_location = fields.Boolean('Is a Scrap Location?', default=False, help='Check this box to allow using this location to put scrapped/damaged goods.')
return_location = fields.Boolean('Is a Return Location?', help='Check this box to allow using this location as a return location.')
replenish_location = fields.Boolean('Replenish Location', copy=False, compute="_compute_replenish_location", readonly=False, store=True,
help='Activate this function to get all quantities to replenish at this particular location')
removal_strategy_id = fields.Many2one(
'product.removal', 'Removal Strategy',
help="Defines the default method used for suggesting the exact location (shelf) "
"where to take the products from, which lot etc. for this location. "
"This method can be enforced at the product category level, "
"and a fallback is made on the parent locations if none is set here.\n\n"
"FIFO: products/lots that were stocked first will be moved out first.\n"
"LIFO: products/lots that were stocked last will be moved out first.\n"
"Closet location: products/lots closest to the target location will be moved out first.\n"
"FEFO: products/lots with the closest removal date will be moved out first "
"(the availability of this method depends on the \"Expiration Dates\" setting).")
putaway_rule_ids = fields.One2many('stock.putaway.rule', 'location_in_id', 'Putaway Rules')
barcode = fields.Char('Barcode', copy=False)
quant_ids = fields.One2many('stock.quant', 'location_id')
cyclic_inventory_frequency = fields.Integer("Inventory Frequency (Days)", default=0, help=" When different than 0, inventory count date for products stored at this location will be automatically set at the defined frequency.")
last_inventory_date = fields.Date("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.")
next_inventory_date = fields.Date("Next Expected Inventory", compute="_compute_next_inventory_date", store=True, help="Date for next planned inventory based on cyclic schedule.")
warehouse_view_ids = fields.One2many('stock.warehouse', 'view_location_id', readonly=True)
warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id', store=True)
storage_category_id = fields.Many2one('stock.storage.category', string='Storage Category', check_company=True)
outgoing_move_line_ids = fields.One2many('stock.move.line', 'location_id') # used to compute weight
incoming_move_line_ids = fields.One2many('stock.move.line', 'location_dest_id') # used to compute weight
net_weight = fields.Float('Net Weight', compute="_compute_weight")
forecast_weight = fields.Float('Forecasted Weight', compute="_compute_weight")
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company!'),
('inventory_freq_nonneg', 'check(cyclic_inventory_frequency >= 0)', 'The inventory frequency (days) for a location must be non-negative')]
@api.depends('outgoing_move_line_ids.quantity_product_uom', 'incoming_move_line_ids.quantity_product_uom',
'outgoing_move_line_ids.state', 'incoming_move_line_ids.state',
'outgoing_move_line_ids.product_id.weight', 'outgoing_move_line_ids.product_id.weight',
'quant_ids.quantity', 'quant_ids.product_id.weight')
@api.depends_context('exclude_sml_ids')
def _compute_weight(self):
for location in self:
location.net_weight = 0
quants = location.quant_ids.filtered(lambda q: q.product_id.type != 'service')
excluded_sml_ids = self._context.get('exclude_sml_ids', [])
incoming_move_lines = location.incoming_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'] and ml.id not in excluded_sml_ids)
outgoing_move_lines = location.outgoing_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'] and ml.id not in excluded_sml_ids)
for quant in quants:
location.net_weight += quant.product_id.weight * quant.quantity
location.forecast_weight = location.net_weight
for line in incoming_move_lines:
location.forecast_weight += line.product_id.weight * line.quantity_product_uom
for line in outgoing_move_lines:
location.forecast_weight -= line.product_id.weight * line.quantity_product_uom
@api.depends('name', 'location_id.complete_name', 'usage')
def _compute_complete_name(self):
for location in self:
if location.location_id and location.usage != 'view':
location.complete_name = '%s/%s' % (location.location_id.complete_name, location.name)
else:
location.complete_name = location.name
@api.depends('cyclic_inventory_frequency', 'last_inventory_date', 'usage', 'company_id')
def _compute_next_inventory_date(self):
for location in self:
if location.company_id and location.usage in ['internal', 'transit'] and location.cyclic_inventory_frequency > 0:
try:
if location.last_inventory_date:
days_until_next_inventory = location.cyclic_inventory_frequency - (fields.Date.today() - location.last_inventory_date).days
if days_until_next_inventory <= 0:
location.next_inventory_date = fields.Date.today() + timedelta(days=1)
else:
location.next_inventory_date = location.last_inventory_date + timedelta(days=location.cyclic_inventory_frequency)
else:
location.next_inventory_date = fields.Date.today() + timedelta(days=location.cyclic_inventory_frequency)
except OverflowError:
raise UserError(_("The selected Inventory Frequency (Days) creates a date too far into the future."))
else:
location.next_inventory_date = False
@api.depends('warehouse_view_ids', 'location_id')
def _compute_warehouse_id(self):
warehouses = self.env['stock.warehouse'].search([('view_location_id', 'parent_of', self.ids)])
warehouses = warehouses.sorted(lambda w: w.view_location_id.parent_path, reverse=True)
view_by_wh = OrderedDict((wh.view_location_id.id, wh.id) for wh in warehouses)
self.warehouse_id = False
for loc in self:
if not loc.parent_path:
continue
path = set(int(loc_id) for loc_id in loc.parent_path.split('/')[:-1])
for view_location_id in view_by_wh:
if view_location_id in path:
loc.warehouse_id = view_by_wh[view_location_id]
break
@api.depends('child_ids.usage', 'child_ids.child_internal_location_ids')
def _compute_child_internal_location_ids(self):
# batch reading optimization is not possible because the field has recursive=True
for loc in self:
loc.child_internal_location_ids = self.search([('id', 'child_of', loc.id), ('usage', '=', 'internal')])
@api.onchange('usage')
def _onchange_usage(self):
if self.usage not in ('internal', 'inventory'):
self.scrap_location = False
@api.depends('usage')
def _compute_replenish_location(self):
for loc in self:
if loc.usage != 'internal':
loc.replenish_location = False
@api.constrains('replenish_location', 'location_id', 'usage')
def _check_replenish_location(self):
for loc in self:
if loc.replenish_location:
# cannot have parent/child location set as replenish as well
replenish_wh_location = self.search([('id', '!=', loc.id), ('replenish_location', '=', True), '|', ('location_id', 'child_of', loc.id), ('location_id', 'parent_of', loc.id)], limit=1)
if replenish_wh_location:
raise ValidationError(_('Another parent/sub replenish location %s exists, if you wish to change it, uncheck it first', replenish_wh_location.name))
@api.constrains('scrap_location')
def _check_scrap_location(self):
for record in self:
if record.scrap_location and self.env['stock.picking.type'].search([('code', '=', 'mrp_operation'), ('default_location_dest_id', '=', record.id)]):
raise ValidationError(_("You cannot set a location as a scrap location when it assigned as a destination location for a manufacturing type operation."))
def write(self, values):
if 'company_id' in values:
for location in self:
if location.company_id.id != values['company_id']:
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
if 'usage' in values and values['usage'] == 'view':
if self.mapped('quant_ids'):
raise UserError(_("This location's usage cannot be changed to view as it contains products."))
if 'usage' in values or 'scrap_location' in values:
modified_locations = self.filtered(
lambda l: any(l[f] != values[f] if f in values else False
for f in {'usage', 'scrap_location'}))
reserved_quantities = self.env['stock.move.line'].search_count([
('location_id', 'in', modified_locations.ids),
('quantity_product_uom', '>', 0),
])
if reserved_quantities:
raise UserError(_(
"You cannot change the location type or its use as a scrap"
" location as there are products reserved in this location."
" Please unreserve the products first."
))
if 'active' in values:
if not values['active']:
for location in self:
warehouses = self.env['stock.warehouse'].search([('active', '=', True), '|', ('lot_stock_id', '=', location.id), ('view_location_id', '=', location.id)], limit=1)
if warehouses:
raise UserError(_(
"You cannot archive the location %s as it is used by your warehouse %s",
location.display_name, warehouses.display_name))
if not self.env.context.get('do_not_check_quant'):
children_location = self.env['stock.location'].with_context(active_test=False).search([('id', 'child_of', self.ids)])
internal_children_locations = children_location.filtered(lambda l: l.usage == 'internal')
children_quants = self.env['stock.quant'].search(['&', '|', ('quantity', '!=', 0), ('reserved_quantity', '!=', 0), ('location_id', 'in', internal_children_locations.ids)])
if children_quants and not values['active']:
raise UserError(_(
"You can't disable locations %s because they still contain products.",
', '.join(children_quants.mapped('location_id.display_name'))))
else:
super(Location, children_location - self).with_context(do_not_check_quant=True).write({
'active': values['active'],
})
res = super().write(values)
self.invalidate_model(['warehouse_id'])
return res
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
self.invalidate_model(['warehouse_id'])
return res
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
default = dict(default or {})
if 'name' not in default:
default['name'] = _("%s (copy)", self.name)
return super().copy(default=default)
def _get_putaway_strategy(self, product, quantity=0, package=None, packaging=None, additional_qty=None):
"""Returns the location where the product has to be put, if any compliant
putaway strategy is found. Otherwise returns self.
The quantity should be in the default UOM of the product, it is used when
no package is specified.
"""
self = self._check_access_putaway()
products = self.env.context.get('products', self.env['product.product'])
products |= product
# find package type on package or packaging
package_type = self.env['stock.package.type']
if package:
package_type = package.package_type_id
elif packaging:
package_type = packaging.package_type_id
categ = products.categ_id if len(products.categ_id) == 1 else self.env['product.category']
categs = categ
while categ.parent_id:
categ = categ.parent_id
categs |= categ
putaway_rules = self.putaway_rule_ids.filtered(lambda rule:
(not rule.product_id or rule.product_id in products) and
(not rule.category_id or rule.category_id in categs) and
(not rule.package_type_ids or package_type in rule.package_type_ids))
putaway_rules = putaway_rules.sorted(lambda rule: (rule.package_type_ids,
rule.product_id,
rule.category_id == categs[:1], # same categ, not a parent
rule.category_id),
reverse=True)
putaway_location = None
locations = self.child_internal_location_ids
if putaway_rules:
# get current product qty (qty in current quants and future qty on assigned ml) of all child locations
qty_by_location = defaultdict(lambda: 0)
if locations.storage_category_id:
if package and package.package_type_id:
move_line_data = self.env['stock.move.line']._read_group([
('id', 'not in', self._context.get('exclude_sml_ids', [])),
('result_package_id.package_type_id', '=', package_type.id),
('state', 'not in', ['draft', 'cancel', 'done']),
], ['location_dest_id'], ['result_package_id:count_distinct'])
quant_data = self.env['stock.quant']._read_group([
('package_id.package_type_id', '=', package_type.id),
('location_id', 'in', locations.ids),
], ['location_id'], ['package_id:count_distinct'])
qty_by_location.update({location_dest.id: count for location_dest, count in move_line_data})
for location, count in quant_data:
qty_by_location[location.id] += count
else:
move_line_data = self.env['stock.move.line']._read_group([
('id', 'not in', self._context.get('exclude_sml_ids', [])),
('product_id', '=', product.id),
('location_dest_id', 'in', locations.ids),
('state', 'not in', ['draft', 'done', 'cancel'])
], ['location_dest_id'], ['quantity:array_agg', 'product_uom_id:recordset'])
quant_data = self.env['stock.quant']._read_group([
('product_id', '=', product.id),
('location_id', 'in', locations.ids),
], ['location_id'], ['quantity:sum'])
qty_by_location.update({location.id: quantity_sum for location, quantity_sum in quant_data})
for location_dest, quantity_list, uoms in move_line_data:
quantity = sum(ml_uom._compute_quantity(float(qty), product.uom_id) for qty, ml_uom in zip(quantity_list, uoms))
qty_by_location[location_dest.id] += quantity
if additional_qty:
for location_id, qty in additional_qty.items():
qty_by_location[location_id] += qty
putaway_location = putaway_rules._get_putaway_location(product, quantity, package, packaging, qty_by_location)
if not putaway_location:
putaway_location = locations[0] if locations and self.usage == 'view' else self
return putaway_location
def _get_next_inventory_date(self):
""" Used to get the next inventory date for a quant located in this location. It is
based on:
1. Does the location have a cyclic inventory set?
2. If not 1, then is there an annual inventory date set (for its company)?
3. If not 1 and 2, then quants have no next inventory date."""
if self.usage not in ['internal', 'transit']:
return False
next_inventory_date = False
if self.next_inventory_date:
next_inventory_date = self.next_inventory_date
elif self.company_id.annual_inventory_month:
today = fields.Date.today()
annual_inventory_month = int(self.company_id.annual_inventory_month)
# Manage 0 and negative annual_inventory_day
annual_inventory_day = max(self.company_id.annual_inventory_day, 1)
max_day = calendar.monthrange(today.year, annual_inventory_month)[1]
# Manage annual_inventory_day bigger than last_day
annual_inventory_day = min(annual_inventory_day, max_day)
next_inventory_date = today.replace(
month=annual_inventory_month, day=annual_inventory_day)
if next_inventory_date <= today:
# Manage leap year with the february
max_day = calendar.monthrange(today.year + 1, annual_inventory_month)[1]
annual_inventory_day = min(annual_inventory_day, max_day)
next_inventory_date = next_inventory_date.replace(
day=annual_inventory_day, year=today.year + 1)
return next_inventory_date
def should_bypass_reservation(self):
self.ensure_one()
return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id)
def _check_access_putaway(self):
return self
def _check_can_be_used(self, product, quantity=0, package=None, location_qty=0):
"""Check if product/package can be stored in the location. Quantity
should in the default uom of product, it's only used when no package is
specified."""
self.ensure_one()
if self.storage_category_id:
# check if enough space
if package and package.package_type_id:
# check weight
package_smls = self.env['stock.move.line'].search([('result_package_id', '=', package.id), ('state', 'not in', ['done', 'cancel'])])
if self.storage_category_id.max_weight < self.forecast_weight + sum(package_smls.mapped(lambda sml: sml.quantity_product_uom * sml.product_id.weight)):
return False
# check if enough space
package_capacity = self.storage_category_id.package_capacity_ids.filtered(lambda pc: pc.package_type_id == package.package_type_id)
if package_capacity and location_qty >= package_capacity.quantity:
return False
else:
# check weight
if self.storage_category_id.max_weight < self.forecast_weight + product.weight * quantity:
return False
product_capacity = self.storage_category_id.product_capacity_ids.filtered(lambda pc: pc.product_id == product)
# To handle new line without quantity in order to avoid suggesting a location already full
if product_capacity and location_qty >= product_capacity.quantity:
return False
if product_capacity and quantity + location_qty > product_capacity.quantity:
return False
positive_quant = self.quant_ids.filtered(lambda q: float_compare(q.quantity, 0, precision_rounding=q.product_id.uom_id.rounding) > 0)
# check if only allow new product when empty
if self.storage_category_id.allow_new_product == "empty" and positive_quant:
return False
# check if only allow same product
if self.storage_category_id.allow_new_product == "same":
# In case it's a package, `product` is not defined, so try to get
# the package products from the context
product = product or self._context.get('products')
if (positive_quant and positive_quant.product_id != product) or len(product) > 1:
return False
if self.env['stock.move.line'].search([
('product_id', '!=', product.id),
('state', 'not in', ('done', 'cancel')),
('location_dest_id', '=', self.id),
], limit=1):
return False
return True
class StockRoute(models.Model):
_name = 'stock.route'
_description = "Inventory Routes"
_order = 'sequence'
_check_company_auto = True
name = fields.Char('Route', required=True, translate=True)
active = fields.Boolean('Active', default=True, help="If the active field is set to False, it will allow you to hide the route without removing it.")
sequence = fields.Integer('Sequence', default=0)
rule_ids = fields.One2many('stock.rule', 'route_id', 'Rules', copy=True)
product_selectable = fields.Boolean('Applicable on Product', default=True, help="When checked, the route will be selectable in the Inventory tab of the Product form.")
product_categ_selectable = fields.Boolean('Applicable on Product Category', help="When checked, the route will be selectable on the Product Category.")
warehouse_selectable = fields.Boolean('Applicable on Warehouse', help="When a warehouse is selected for this route, this route should be seen as the default route when products pass through this warehouse.")
packaging_selectable = fields.Boolean('Applicable on Packaging', help="When checked, the route will be selectable on the Product Packaging.")
supplied_wh_id = fields.Many2one('stock.warehouse', 'Supplied Warehouse')
supplier_wh_id = fields.Many2one('stock.warehouse', 'Supplying Warehouse')
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company, index=True,
help='Leave this field empty if this route is shared between all companies')
product_ids = fields.Many2many(
'product.template', 'stock_route_product', 'route_id', 'product_id',
'Products', copy=False, check_company=True)
categ_ids = fields.Many2many('product.category', 'stock_route_categ', 'route_id', 'categ_id', 'Product Categories', copy=False)
packaging_ids = fields.Many2many('product.packaging', 'stock_route_packaging', 'route_id', 'packaging_id', 'Packagings', copy=False, check_company=True)
warehouse_domain_ids = fields.One2many('stock.warehouse', compute='_compute_warehouses')
warehouse_ids = fields.Many2many(
'stock.warehouse', 'stock_route_warehouse', 'route_id', 'warehouse_id',
'Warehouses', copy=False, domain="[('id', 'in', warehouse_domain_ids)]")
def copy(self, default=None):
self.ensure_one()
default = dict(default or {})
if 'name' not in default:
default['name'] = _("%s (copy)", self.name)
return super().copy(default=default)
@api.depends('company_id')
def _compute_warehouses(self):
for loc in self:
domain = [('company_id', '=', loc.company_id.id)] if loc.company_id else []
loc.warehouse_domain_ids = self.env['stock.warehouse'].search(domain)
@api.onchange('company_id')
def _onchange_company(self):
if self.company_id:
self.warehouse_ids = self.warehouse_ids.filtered(lambda w: w.company_id == self.company_id)
@api.onchange('warehouse_selectable')
def _onchange_warehouse_selectable(self):
if not self.warehouse_selectable:
self.warehouse_ids = [(5, 0, 0)]
def toggle_active(self):
for route in self:
route.with_context(active_test=False).rule_ids.filtered(lambda ru: ru.active == route.active).toggle_active()
super().toggle_active()

292
models/stock_lot.py Normal file
View File

@ -0,0 +1,292 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import operator as py_operator
from operator import attrgetter
from re import findall as regex_findall, split as regex_split
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
OPERATORS = {
'<': py_operator.lt,
'>': py_operator.gt,
'<=': py_operator.le,
'>=': py_operator.ge,
'=': py_operator.eq,
'!=': py_operator.ne
}
class StockLot(models.Model):
_name = 'stock.lot'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Lot/Serial'
_check_company_auto = True
_order = 'name, id'
def _read_group_location_id(self, locations, domain, order):
partner_locations = locations.search([('usage', 'in', ('customer', 'supplier'))])
return partner_locations + locations.warehouse_id.search([]).lot_stock_id
name = fields.Char(
'Lot/Serial Number', default=lambda self: self.env['ir.sequence'].next_by_code('stock.lot.serial'),
required=True, help="Unique Lot/Serial Number", index='trigram')
ref = fields.Char('Internal Reference', help="Internal reference number in case it differs from the manufacturer's lot/serial number")
product_id = fields.Many2one(
'product.product', 'Product', index=True,
domain=("[('tracking', '!=', 'none'), ('type', '=', 'product')] +"
" ([('product_tmpl_id', '=', context['default_product_tmpl_id'])] if context.get('default_product_tmpl_id') else [])"),
required=True, check_company=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure',
related='product_id.uom_id', store=True)
quant_ids = fields.One2many('stock.quant', 'lot_id', 'Quants', readonly=True)
product_qty = fields.Float('On Hand Quantity', compute='_product_qty', search='_search_product_qty')
note = fields.Html(string='Description')
display_complete = fields.Boolean(compute='_compute_display_complete')
company_id = fields.Many2one('res.company', 'Company', required=True, index=True, default=lambda self: self.env.company.id)
delivery_ids = fields.Many2many('stock.picking', compute='_compute_delivery_ids', string='Transfers')
delivery_count = fields.Integer('Delivery order count', compute='_compute_delivery_ids')
last_delivery_partner_id = fields.Many2one('res.partner', compute='_compute_last_delivery_partner_id')
lot_properties = fields.Properties('Properties', definition='product_id.lot_properties_definition', copy=True)
location_id = fields.Many2one(
'stock.location', 'Location', compute='_compute_single_location', store=True, readonly=False,
inverse='_set_single_location', domain="[('usage', '!=', 'view')]", group_expand='_read_group_location_id')
@api.model
def generate_lot_names(self, first_lot, count):
"""Generate `lot_names` from a string."""
# We look if the first lot contains at least one digit.
caught_initial_number = regex_findall(r"\d+", first_lot)
if not caught_initial_number:
return self.generate_lot_names(first_lot + "0", count)
# We base the series on the last number found in the base lot.
initial_number = caught_initial_number[-1]
padding = len(initial_number)
# We split the lot name to get the prefix and suffix.
splitted = regex_split(initial_number, first_lot)
# initial_number could appear several times, e.g. BAV023B00001S00001
prefix = initial_number.join(splitted[:-1])
suffix = splitted[-1]
initial_number = int(initial_number)
return [{
'lot_name': '%s%s%s' % (prefix, str(initial_number + i).zfill(padding), suffix),
} for i in range(0, count)]
@api.model
def _get_next_serial(self, company, product):
"""Return the next serial number to be attributed to the product."""
if product.tracking != "none":
last_serial = self.env['stock.lot'].search(
[('company_id', '=', company.id), ('product_id', '=', product.id)],
limit=1, order='id DESC')
if last_serial:
return self.env['stock.lot'].generate_lot_names(last_serial.name, 2)[1]['lot_name']
return False
@api.constrains('name', 'product_id', 'company_id')
def _check_unique_lot(self):
domain = [('product_id', 'in', self.product_id.ids),
('company_id', 'in', self.company_id.ids),
('name', 'in', self.mapped('name'))]
groupby = ['company_id', 'product_id', 'name']
records = self._read_group(domain, groupby, having=[('__count', '>', 1)])
error_message_lines = []
for __, product, name in records:
error_message_lines.append(_(" - Product: %s, Serial Number: %s", product.display_name, name))
if error_message_lines:
raise ValidationError(_('The combination of serial number and product must be unique across a company.\nFollowing combination contains duplicates:\n') + '\n'.join(error_message_lines))
def _check_create(self):
active_picking_id = self.env.context.get('active_picking_id', False)
if active_picking_id:
picking_id = self.env['stock.picking'].browse(active_picking_id)
if picking_id and not picking_id.picking_type_id.use_create_lots:
raise UserError(_('You are not allowed to create a lot or serial number with this operation type. To change this, go on the operation type and tick the box "Create New Lots/Serial Numbers".'))
@api.depends('name')
def _compute_display_complete(self):
""" Defines if we want to display all fields in the stock.production.lot form view.
It will if the record exists (`id` set) or if we precised it into the context.
This compute depends on field `name` because as it has always a default value, it'll be
always triggered.
"""
for prod_lot in self:
prod_lot.display_complete = prod_lot.id or self._context.get('display_complete')
def _compute_delivery_ids(self):
delivery_ids_by_lot = self._find_delivery_ids_by_lot()
for lot in self:
lot.delivery_ids = delivery_ids_by_lot[lot.id]
lot.delivery_count = len(lot.delivery_ids)
def _compute_last_delivery_partner_id(self):
serial_products = self.filtered(lambda l: l.product_id.tracking == 'serial')
delivery_ids_by_lot = serial_products._find_delivery_ids_by_lot()
(self - serial_products).last_delivery_partner_id = False
for lot in serial_products:
if lot.product_id.tracking == 'serial' and len(delivery_ids_by_lot[lot.id]) > 0:
lot.last_delivery_partner_id = self.env['stock.picking'].browse(delivery_ids_by_lot[lot.id]).sorted(key='date_done', reverse=True)[0].partner_id
else:
lot.last_delivery_partner_id = False
@api.depends('quant_ids')
def _compute_single_location(self):
for lot in self:
quants = lot.quant_ids.filtered(lambda q: q.quantity > 0)
lot.location_id = quants.location_id if len(quants.location_id) == 1 else False
def _set_single_location(self):
quants = self.quant_ids.filtered(lambda q: q.quantity > 0)
if len(quants.location_id) == 1:
unpack = len(quants.package_id.quant_ids) > 1
quants.move_quants(location_dest_id=self.location_id, message=_("Lot/Serial Number Relocated"), unpack=unpack)
elif len(quants.location_id) > 1:
raise UserError(_('You can only move a lot/serial to a new location if it exists in a single location.'))
@api.model_create_multi
def create(self, vals_list):
self._check_create()
return super(StockLot, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
def write(self, vals):
if 'company_id' in vals:
for lot in self:
if lot.company_id.id != vals['company_id']:
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
if 'product_id' in vals and any(vals['product_id'] != lot.product_id.id for lot in self):
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids), ('product_id', '!=', vals['product_id'])])
if move_lines:
raise UserError(_(
'You are not allowed to change the product linked to a serial or lot number '
'if some stock moves have already been created with that number. '
'This would lead to inconsistencies in your stock.'
))
return super().write(vals)
def copy(self, default=None):
if default is None:
default = {}
if 'name' not in default:
default['name'] = _("(copy of) %s", self.name)
return super().copy(default)
@api.depends('quant_ids', 'quant_ids.quantity')
def _product_qty(self):
for lot in self:
# We only care for the quants in internal or transit locations.
quants = lot.quant_ids.filtered(lambda q: q.location_id.usage == 'internal' or (q.location_id.usage == 'transit' and q.location_id.company_id))
lot.product_qty = sum(quants.mapped('quantity'))
def _search_product_qty(self, operator, value):
if operator not in OPERATORS:
raise UserError(_("Invalid domain operator %s", operator))
if not isinstance(value, (float, int)):
raise UserError(_("Invalid domain right operand '%s'. It must be of type Integer/Float", value))
domain = [
('lot_id', '!=', False),
'|', ('location_id.usage', '=', 'internal'),
'&', ('location_id.usage', '=', 'transit'), ('location_id.company_id', '!=', False)
]
lots_w_qty = self.env['stock.quant']._read_group(domain=domain, groupby=['lot_id'], aggregates=['quantity:sum'], having=[('quantity:sum', '!=', 0)])
ids = []
lot_ids_w_qty = []
for lot, quantity_sum in lots_w_qty:
lot_id = lot.id
lot_ids_w_qty.append(lot_id)
if OPERATORS[operator](quantity_sum, value):
ids.append(lot_id)
if value == 0.0 and operator == '=':
return [('id', 'not in', lot_ids_w_qty)]
if value == 0.0 and operator == '!=':
return [('id', 'in', lot_ids_w_qty)]
# check if we need include zero values in result
include_zero = (
value < 0.0 and operator in ('>', '>=') or
value > 0.0 and operator in ('<', '<=') or
value == 0.0 and operator in ('>=', '<=')
)
if include_zero:
return ['|', ('id', 'in', ids), ('id', 'not in', lot_ids_w_qty)]
return [('id', 'in', ids)]
def action_lot_open_quants(self):
self = self.with_context(search_default_lot_id=self.id, create=False)
if self.user_has_groups('stock.group_stock_manager'):
self = self.with_context(inventory_mode=True)
return self.env['stock.quant'].action_view_quants()
def action_lot_open_transfers(self):
self.ensure_one()
action = {
'res_model': 'stock.picking',
'type': 'ir.actions.act_window'
}
if len(self.delivery_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': self.delivery_ids[0].id
})
else:
action.update({
'name': _("Delivery orders of %s", self.display_name),
'domain': [('id', 'in', self.delivery_ids.ids)],
'view_mode': 'tree,form'
})
return action
@api.model
def _get_outgoing_domain(self):
return [
'|',
('picking_code', '=', 'outgoing'),
('produce_line_ids', '!=', False),
]
def _find_delivery_ids_by_lot(self, lot_path=None, delivery_by_lot=None):
if lot_path is None:
lot_path = set()
domain = [
('lot_id', 'in', self.ids),
('state', '=', 'done'),
]
domain_restriction = self._get_outgoing_domain()
domain = expression.AND([domain, domain_restriction])
move_lines = self.env['stock.move.line'].search(domain)
moves_by_lot = {
lot_id: {'producing_lines': set(), 'barren_lines': set()}
for lot_id in move_lines.lot_id.ids
}
for line in move_lines:
if line.produce_line_ids:
moves_by_lot[line.lot_id.id]['producing_lines'].add(line.id)
else:
moves_by_lot[line.lot_id.id]['barren_lines'].add(line.id)
if delivery_by_lot is None:
delivery_by_lot = dict()
for lot in self:
delivery_ids = set()
if moves_by_lot.get(lot.id):
producing_move_lines = self.env['stock.move.line'].browse(moves_by_lot[lot.id]['producing_lines'])
barren_move_lines = self.env['stock.move.line'].browse(moves_by_lot[lot.id]['barren_lines'])
if producing_move_lines:
lot_path.add(lot.id)
next_lots = producing_move_lines.produce_line_ids.lot_id.filtered(lambda l: l.id not in lot_path)
next_lots_ids = set(next_lots.ids)
# If some producing lots are in lot_path, it means that they have been previously processed.
# Their results are therefore already in delivery_by_lot and we add them to delivery_ids directly.
delivery_ids.update(*(delivery_by_lot.get(lot_id, []) for lot_id in (producing_move_lines.produce_line_ids.lot_id - next_lots).ids))
for lot_id, delivery_ids_set in next_lots._find_delivery_ids_by_lot(lot_path=lot_path, delivery_by_lot=delivery_by_lot).items():
if lot_id in next_lots_ids:
delivery_ids.update(delivery_ids_set)
delivery_ids.update(barren_move_lines.picking_id.ids)
delivery_by_lot[lot.id] = list(delivery_ids)
return delivery_by_lot

2215
models/stock_move.py Normal file

File diff suppressed because it is too large Load Diff

953
models/stock_move_line.py Normal file
View File

@ -0,0 +1,953 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import Counter, defaultdict
from odoo import _, api, fields, tools, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import OrderedSet, groupby
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
class StockMoveLine(models.Model):
_name = "stock.move.line"
_description = "Product Moves (Stock Move Line)"
_rec_name = "product_id"
_order = "result_package_id desc, id"
picking_id = fields.Many2one(
'stock.picking', 'Transfer', auto_join=True,
check_company=True,
index=True,
help='The stock operation where the packing has been made')
move_id = fields.Many2one(
'stock.move', 'Stock Operation',
check_company=True, index=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True, index=True)
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade", check_company=True, domain="[('type', '!=', 'service')]", index=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure', required=True, domain="[('category_id', '=', product_uom_category_id)]",
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
)
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_category_name = fields.Char(related="product_id.categ_id.complete_name", store=True, string="Product Category")
quantity = fields.Float(
'Quantity', digits='Product Unit of Measure', copy=False, store=True,
compute='_compute_quantity', readonly=False)
quantity_product_uom = fields.Float(
'Quantity in Product UoM', digits='Product Unit of Measure',
copy=False, compute='_compute_quantity_product_uom', store=True)
picked = fields.Boolean('Picked', compute='_compute_picked', store=True, readonly=False, copy=False)
package_id = fields.Many2one(
'stock.quant.package', 'Source Package', ondelete='restrict',
check_company=True,
domain="[('location_id', '=', location_id)]")
package_level_id = fields.Many2one('stock.package_level', 'Package Level', check_company=True)
lot_id = fields.Many2one(
'stock.lot', 'Lot/Serial Number',
domain="[('product_id', '=', product_id)]", check_company=True)
lot_name = fields.Char('Lot/Serial Number Name')
result_package_id = fields.Many2one(
'stock.quant.package', 'Destination Package',
ondelete='restrict', required=False, check_company=True,
domain="['|', '|', ('location_id', '=', False), ('location_id', '=', location_dest_id), ('id', '=', package_id)]",
help="If set, the operations are packed into this package")
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
owner_id = fields.Many2one(
'res.partner', 'From Owner',
check_company=True,
help="When validating the transfer, the products will be taken from this owner.")
location_id = fields.Many2one(
'stock.location', 'From', domain="[('usage', '!=', 'view')]", check_company=True, required=True,
compute="_compute_location_id", store=True, readonly=False, precompute=True,
)
location_dest_id = fields.Many2one('stock.location', 'To', domain="[('usage', '!=', 'view')]", check_company=True, required=True, compute="_compute_location_id", store=True, readonly=False, precompute=True)
location_usage = fields.Selection(string="Source Location Type", related='location_id.usage')
location_dest_usage = fields.Selection(string="Destination Location Type", related='location_dest_id.usage')
lots_visible = fields.Boolean(compute='_compute_lots_visible')
picking_partner_id = fields.Many2one(related='picking_id.partner_id', readonly=True)
picking_code = fields.Selection(related='picking_type_id.code', readonly=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation type', compute='_compute_picking_type_id', search='_search_picking_type_id')
picking_type_use_create_lots = fields.Boolean(related='picking_type_id.use_create_lots', readonly=True)
picking_type_use_existing_lots = fields.Boolean(related='picking_type_id.use_existing_lots', readonly=True)
picking_type_entire_packs = fields.Boolean(related='picking_id.picking_type_id.show_entire_packs', readonly=True)
state = fields.Selection(related='move_id.state', store=True, related_sudo=False)
is_inventory = fields.Boolean(related='move_id.is_inventory')
is_locked = fields.Boolean(related='move_id.is_locked', readonly=True)
consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id')
produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id')
reference = fields.Char(related='move_id.reference', store=True, related_sudo=False, readonly=False)
tracking = fields.Selection(related='product_id.tracking', readonly=True)
origin = fields.Char(related='move_id.origin', string='Source')
description_picking = fields.Text(string="Description picking")
quant_id = fields.Many2one('stock.quant', "Pick From", store=False) # Dummy field for the detailed operation view
product_packaging_qty = fields.Float(string="Reserved Packaging Quantity", compute='_compute_product_packaging_qty')
picking_location_id = fields.Many2one(related='picking_id.location_id')
picking_location_dest_id = fields.Many2one(related='picking_id.location_dest_id')
@api.depends('product_uom_id.category_id', 'product_id.uom_id.category_id', 'move_id.product_uom', 'product_id.uom_id')
def _compute_product_uom_id(self):
for line in self:
if not line.product_uom_id or line.product_uom_id.category_id != line.product_id.uom_id.category_id:
if line.move_id.product_uom:
line.product_uom_id = line.move_id.product_uom.id
else:
line.product_uom_id = line.product_id.uom_id.id
@api.depends('picking_id.picking_type_id', 'product_id.tracking')
def _compute_lots_visible(self):
for line in self:
picking = line.picking_id
if picking.picking_type_id and line.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
line.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
else:
line.lots_visible = line.product_id.tracking != 'none'
@api.depends('state')
def _compute_picked(self):
for line in self:
if line.move_id.state == 'done':
line.picked = True
@api.depends('picking_id')
def _compute_picking_type_id(self):
self.picking_type_id = False
for line in self:
if line.picking_id:
line.picking_type_id = line.picking_id.picking_type_id
@api.depends('move_id', 'move_id.location_id', 'move_id.location_dest_id')
def _compute_location_id(self):
for line in self:
if not line.location_id:
line.location_id = line.move_id.location_id or line.picking_id.location_id
if not line.location_dest_id:
line.location_dest_id = line.move_id.location_dest_id or line.picking_id.location_dest_id
@api.depends('move_id.product_packaging_id', 'product_uom_id', 'quantity')
def _compute_product_packaging_qty(self):
self.product_packaging_qty = 0
for line in self:
if not line.move_id.product_packaging_id:
continue
line.product_packaging_qty = line.move_id.product_packaging_id._compute_qty(line.quantity, line.product_uom_id)
def _search_picking_type_id(self, operator, value):
return [('picking_id.picking_type_id', operator, value)]
@api.depends('quant_id')
def _compute_quantity(self):
for record in self:
if not record.quant_id or record.quantity:
continue
origin_move = record.move_id._origin
if float_compare(record.move_id.product_qty, origin_move.quantity, record.move_id.product_uom.rounding) > 0:
record.quantity = max(0, min(record.quant_id.available_quantity, record.move_id.product_qty - origin_move.quantity))
else:
record.quantity = max(0, record.quant_id.available_quantity)
@api.depends('quantity', 'product_uom_id')
def _compute_quantity_product_uom(self):
for line in self:
line.quantity_product_uom = line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id, rounding_method='HALF-UP')
@api.constrains('lot_id', 'product_id')
def _check_lot_product(self):
for line in self:
if line.lot_id and line.product_id != line.lot_id.sudo().product_id:
raise ValidationError(_(
'This lot %(lot_name)s is incompatible with this product %(product_name)s',
lot_name=line.lot_id.name,
product_name=line.product_id.display_name
))
@api.constrains('quantity')
def _check_positive_quantity(self):
if any(ml.quantity < 0 for ml in self):
raise ValidationError(_('You can not enter negative quantities.'))
@api.onchange('product_id', 'product_uom_id')
def _onchange_product_id(self):
if self.product_id:
if self.picking_id:
product = self.product_id.with_context(lang=self.picking_id.partner_id.lang or self.env.user.lang)
self.description_picking = product._get_description(self.picking_id.picking_type_id)
self.lots_visible = self.product_id.tracking != 'none'
@api.onchange('lot_name', 'lot_id')
def _onchange_serial_number(self):
""" When the user is encoding a move line for a tracked product, we apply some logic to
help him. This includes:
- automatically switch `quantity` to 1.0
- warn if he has already encoded `lot_name` in another move line
- warn (and update if appropriate) if the SN is in a different source location than selected
"""
res = {}
if self.product_id.tracking == 'serial':
if not self.quantity:
self.quantity = 1
message = None
if self.lot_name or self.lot_id:
move_lines_to_check = self._get_similar_move_lines() - self
if self.lot_name:
counter = Counter([line.lot_name for line in move_lines_to_check])
if counter.get(self.lot_name) and counter[self.lot_name] > 1:
message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
elif not self.lot_id:
lots = self.env['stock.lot'].search([('product_id', '=', self.product_id.id),
('name', '=', self.lot_name),
('company_id', '=', self.company_id.id)])
quants = lots.quant_ids.filtered(lambda q: q.quantity != 0 and q.location_id.usage in ['customer', 'internal', 'transit'])
if quants:
message = _('Serial number (%s) already exists in location(s): %s. Please correct the serial number encoded.', self.lot_name, ', '.join(quants.location_id.mapped('display_name')))
elif self.lot_id:
counter = Counter([line.lot_id.id for line in move_lines_to_check])
if counter.get(self.lot_id.id) and counter[self.lot_id.id] > 1:
message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
else:
# check if in correct source location
message, recommended_location = self.env['stock.quant'].sudo()._check_serial_number(
self.product_id, self.lot_id, self.company_id, self.location_id, self.picking_id.location_id)
if recommended_location:
self.location_id = recommended_location
if message:
res['warning'] = {'title': _('Warning'), 'message': message}
return res
@api.onchange('quantity', 'product_uom_id')
def _onchange_quantity(self):
""" When the user is encoding a move line for a tracked product, we apply some logic to
help him. This onchange will warn him if he set `quantity` to a non-supported value.
"""
res = {}
if self.quantity and self.product_id.tracking == 'serial':
if float_compare(self.quantity_product_uom, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0 and not float_is_zero(self.quantity_product_uom, precision_rounding=self.product_id.uom_id.rounding):
raise UserError(_('You can only process 1.0 %s of products with unique serial number.', self.product_id.uom_id.name))
return res
@api.onchange('result_package_id', 'product_id', 'product_uom_id', 'quantity')
def _onchange_putaway_location(self):
default_dest_location = self._get_default_dest_location()
if not self.id and self.user_has_groups('stock.group_stock_multi_locations') and self.product_id and self.quantity_product_uom \
and self.location_dest_id == default_dest_location:
quantity = self.quantity_product_uom
self.location_dest_id = default_dest_location.with_context(exclude_sml_ids=self.ids)._get_putaway_strategy(
self.product_id, quantity=quantity, package=self.result_package_id,
packaging=self.move_id.product_packaging_id)
def _apply_putaway_strategy(self):
if self._context.get('avoid_putaway_rules'):
return
self = self.with_context(do_not_unreserve=True)
for package, smls in groupby(self, lambda sml: sml.result_package_id):
smls = self.env['stock.move.line'].concat(*smls)
excluded_smls = smls
if package.package_type_id:
best_loc = smls.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids, products=smls.product_id)._get_putaway_strategy(self.env['product.product'], package=package)
smls.location_dest_id = smls.package_level_id.location_dest_id = best_loc
elif package:
used_locations = set()
for sml in smls:
if len(used_locations) > 1:
break
sml.location_dest_id = sml.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids)._get_putaway_strategy(sml.product_id, quantity=sml.quantity)
excluded_smls -= sml
used_locations.add(sml.location_dest_id)
if len(used_locations) > 1:
smls.location_dest_id = smls.move_id.location_dest_id
else:
smls.package_level_id.location_dest_id = smls.location_dest_id
else:
for sml in smls:
putaway_loc_id = sml.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids)._get_putaway_strategy(
sml.product_id, quantity=sml.quantity, packaging=sml.move_id.product_packaging_id,
)
if putaway_loc_id != sml.location_dest_id:
sml.location_dest_id = putaway_loc_id
excluded_smls -= sml
def _get_default_dest_location(self):
if not self.user_has_groups('stock.group_stock_storage_categories'):
return self.location_dest_id[:1]
if self.env.context.get('default_location_dest_id'):
return self.env['stock.location'].browse([self.env.context.get('default_location_dest_id')])
return (self.move_id.location_dest_id or self.picking_id.location_dest_id or self.location_dest_id)[0]
def _get_putaway_additional_qty(self):
addtional_qty = {}
for ml in self._origin:
qty = ml.product_uom_id._compute_quantity(ml.quantity, ml.product_id.uom_id)
addtional_qty[ml.location_dest_id.id] = addtional_qty.get(ml.location_dest_id.id, 0) - qty
return addtional_qty
def init(self):
if not tools.index_exists(self._cr, 'stock_move_line_free_reservation_index'):
self._cr.execute("""
CREATE INDEX stock_move_line_free_reservation_index
ON
stock_move_line (id, company_id, product_id, lot_id, location_id, owner_id, package_id)
WHERE
(state IS NULL OR state NOT IN ('cancel', 'done')) AND quantity_product_uom > 0 AND not picked""")
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('move_id'):
vals['company_id'] = self.env['stock.move'].browse(vals['move_id']).company_id.id
elif vals.get('picking_id'):
vals['company_id'] = self.env['stock.picking'].browse(vals['picking_id']).company_id.id
if vals.get('move_id') and 'picked' not in vals:
vals['picked'] = self.env['stock.move'].browse(vals['move_id']).picked
if vals.get('quant_id'):
vals.update(self._copy_quant_info(vals))
mls = super().create(vals_list)
def create_move(move_line):
new_move = self.env['stock.move'].create(move_line._prepare_stock_move_vals())
move_line.move_id = new_move.id
# If the move line is directly create on the picking view.
# If this picking is already done we should generate an
# associated done move.
for move_line in mls:
if move_line.move_id or not move_line.picking_id:
continue
if move_line.picking_id.state != 'done':
moves = move_line.picking_id.move_ids.filtered(lambda x: x.product_id == move_line.product_id)
moves = sorted(moves, key=lambda m: m.quantity < m.product_qty, reverse=True)
if moves:
move_line.write({
'move_id': moves[0].id,
'picking_id': moves[0].picking_id.id,
})
else:
create_move(move_line)
else:
create_move(move_line)
move_to_recompute_state = self.env['stock.move']
for move_line in mls:
location = move_line.location_id
product = move_line.product_id
move = move_line.move_id
if move:
reservation = not move._should_bypass_reservation()
else:
reservation = product.type == 'product' and not location.should_bypass_reservation()
if move_line.quantity and reservation:
self.env.context.get('reserved_quant', self.env['stock.quant'])._update_reserved_quantity(
product, location, move_line.quantity_product_uom, lot_id=move_line.lot_id, package_id=move_line.package_id, owner_id=move_line.owner_id)
if move:
move_to_recompute_state |= move
move_to_recompute_state._recompute_state()
for ml, vals in zip(mls, vals_list):
if ml.state == 'done':
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
quantity = ml.product_uom_id._compute_quantity(ml.quantity, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
in_date = None
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if available_qty < 0 and ml.lot_id:
# see if we can compensate the negative quants with some untracked quants
untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
if untracked_qty:
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
next_moves = ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
next_moves._do_unreserve()
next_moves._action_assign()
return mls
def write(self, vals):
if 'product_id' in vals and any(vals.get('state', ml.state) != 'draft' and vals['product_id'] != ml.product_id.id for ml in self):
raise UserError(_("Changing the product is only allowed in 'Draft' state."))
moves_to_recompute_state = self.env['stock.move']
triggers = [
('location_id', 'stock.location'),
('location_dest_id', 'stock.location'),
('lot_id', 'stock.lot'),
('package_id', 'stock.quant.package'),
('result_package_id', 'stock.quant.package'),
('owner_id', 'res.partner'),
('product_uom_id', 'uom.uom')
]
if vals.get('quant_id'):
vals.update(self._copy_quant_info(vals))
updates = {}
for key, model in triggers:
if key in vals:
updates[key] = vals[key] if isinstance(vals[key], models.BaseModel) else self.env[model].browse(vals[key])
if 'result_package_id' in updates:
for ml in self.filtered(lambda ml: ml.package_level_id):
if updates.get('result_package_id'):
ml.package_level_id.package_id = updates.get('result_package_id')
else:
# TODO: make package levels less of a pain and fix this
package_level = ml.package_level_id
ml.package_level_id = False
# Only need to unlink the package level if it's empty. Otherwise will unlink it to still valid move lines.
if not package_level.move_line_ids:
package_level.unlink()
# When we try to write on a reserved move line any fields from `triggers` or directly
# `reserved_uom_qty` (the actual reserved quantity), we need to make sure the associated
# quants are correctly updated in order to not make them out of sync (i.e. the sum of the
# move lines `reserved_uom_qty` should always be equal to the sum of `reserved_quantity` on
# the quants). If the new charateristics are not available on the quants, we chose to
# reserve the maximum possible.
if updates or 'quantity' in vals:
for ml in self:
if ml.product_id.type != 'product' or ml.state == 'done':
continue
if 'quantity' in vals:
new_reserved_qty = ml.product_uom_id._compute_quantity(
vals['quantity'], ml.product_id.uom_id, rounding_method='HALF-UP')
# Make sure `reserved_uom_qty` is not negative.
if float_compare(new_reserved_qty, 0, precision_rounding=ml.product_id.uom_id.rounding) < 0:
raise UserError(_('Reserving a negative quantity is not allowed.'))
else:
new_reserved_qty = ml.quantity_product_uom
# Unreserve the old charateristics of the move line.
if not float_is_zero(ml.quantity_product_uom, precision_rounding=ml.product_uom_id.rounding):
ml._synchronize_quant(-ml.quantity_product_uom, ml.location_id, action="reserved")
# Reserve the maximum available of the new charateristics of the move line.
if not ml.move_id._should_bypass_reservation(updates.get('location_id', ml.location_id)):
ml._synchronize_quant(
new_reserved_qty, updates.get('location_id', ml.location_id), action="reserved",
lot=updates.get('lot_id', ml.lot_id), package=updates.get('package_id', ml.package_id),
owner=updates.get('owner_id', ml.owner_id))
if 'quantity' in vals and vals['quantity'] != ml.quantity:
moves_to_recompute_state |= ml.move_id
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
mls = self.env['stock.move.line']
if updates or 'quantity' in vals:
next_moves = self.env['stock.move']
mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product')
if not updates: # we can skip those where quantity is already good up to UoM rounding
mls = mls.filtered(lambda ml: not float_is_zero(ml.quantity - vals['quantity'], precision_rounding=ml.product_uom_id.rounding))
for ml in mls:
# undo the original move line
in_date = ml._synchronize_quant(-ml.quantity_product_uom, ml.location_dest_id, package=ml.result_package_id)[1]
ml._synchronize_quant(ml.quantity_product_uom, ml.location_id, in_date=in_date)
# Unreserve and reserve following move in order to have the real reserved quantity on move_line.
next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
# Log a note
if ml.picking_id:
ml._log_message(ml.picking_id, ml, 'stock.track_move_template', vals)
res = super(StockMoveLine, self).write(vals)
for ml in mls:
available_qty, dummy = ml._synchronize_quant(-ml.quantity_product_uom, ml.location_id)
ml._synchronize_quant(ml.quantity_product_uom, ml.location_dest_id, package=ml.result_package_id)
if available_qty < 0:
ml._free_reservation(
ml.product_id, ml.location_id,
abs(available_qty), lot_id=ml.lot_id, package_id=ml.package_id,
owner_id=ml.owner_id)
# As stock_account values according to a move's `product_uom_qty`, we consider that any
# done stock move should have its `quantity_done` equals to its `product_uom_qty`, and
# this is what move's `action_done` will do. So, we replicate the behavior here.
if updates or 'quantity' in vals:
next_moves._do_unreserve()
next_moves._action_assign()
if moves_to_recompute_state:
moves_to_recompute_state._recompute_state()
return res
@api.ondelete(at_uninstall=False)
def _unlink_except_done_or_cancel(self):
for ml in self:
if ml.state in ('done', 'cancel'):
raise UserError(_('You can not delete product moves if the picking is done. You can only correct the done quantities.'))
def unlink(self):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
quants_by_product = self.env['stock.quant']._get_quants_by_products_locations(self.product_id, self.location_id)
for ml in self:
# Unlinking a move line should unreserve.
if not float_is_zero(ml.quantity_product_uom, precision_digits=precision) and ml.move_id and not ml.move_id._should_bypass_reservation(ml.location_id):
quants = quants_by_product[ml.product_id.id]
quants._update_reserved_quantity(ml.product_id, ml.location_id, -ml.quantity_product_uom, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
moves = self.mapped('move_id')
package_levels = self.package_level_id
res = super().unlink()
package_levels = package_levels.filtered(lambda pl: not (pl.move_line_ids or pl.move_ids))
if package_levels:
package_levels.unlink()
if moves:
# Add with_prefetch() to set the _prefecht_ids = _ids
# because _prefecht_ids generator look lazily on the cache of move_id
# which is clear by the unlink of move line
moves.with_prefetch()._recompute_state()
return res
def _action_done(self):
""" This method is called during a move's `action_done`. It'll actually move a quant from
the source location to the destination location, and unreserve if needed in the source
location.
This method is intended to be called on all the move lines of a move. This method is not
intended to be called when editing a `done` move (that's what the override of `write` here
is done.
"""
# First, we loop over all the move lines to do a preliminary check: `quantity` should not
# be negative and, according to the presence of a picking type or a linked inventory
# adjustment, enforce some rules on the `lot_id` field. If `quantity` is null, we unlink
# the line. It is mandatory in order to free the reservation and correctly apply
# `action_done` on the next move lines.
ml_ids_tracked_without_lot = OrderedSet()
ml_ids_to_delete = OrderedSet()
ml_ids_to_create_lot = OrderedSet()
for ml in self:
# Check here if `ml.quantity` respects the rounding of `ml.product_uom_id`.
uom_qty = float_round(ml.quantity, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP')
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
quantity = float_round(ml.quantity, precision_digits=precision_digits, rounding_method='HALF-UP')
if float_compare(uom_qty, quantity, precision_digits=precision_digits) != 0:
raise UserError(_('The quantity done for the product "%s" doesn\'t respect the rounding precision '
'defined on the unit of measure "%s". Please change the quantity done or the '
'rounding precision of your unit of measure.',
ml.product_id.display_name, ml.product_uom_id.name))
quantity_float_compared = float_compare(ml.quantity, 0, precision_rounding=ml.product_uom_id.rounding)
if quantity_float_compared > 0:
if ml.product_id.tracking != 'none':
picking_type_id = ml.move_id.picking_type_id
if picking_type_id:
if picking_type_id.use_create_lots:
# If a picking type is linked, we may have to create a production lot on
# the fly before assigning it to the move line if the user checked both
# `use_create_lots` and `use_existing_lots`.
if ml.lot_name and not ml.lot_id:
lot = self.env['stock.lot'].search([
('company_id', '=', ml.company_id.id),
('product_id', '=', ml.product_id.id),
('name', '=', ml.lot_name),
], limit=1)
if lot:
ml.lot_id = lot.id
else:
ml_ids_to_create_lot.add(ml.id)
elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots:
# If the user disabled both `use_create_lots` and `use_existing_lots`
# checkboxes on the picking type, he's allowed to enter tracked
# products without a `lot_id`.
continue
elif ml.is_inventory:
# If an inventory adjustment is linked, the user is allowed to enter
# tracked products without a `lot_id`.
continue
if not ml.lot_id and ml.id not in ml_ids_to_create_lot:
ml_ids_tracked_without_lot.add(ml.id)
elif quantity_float_compared < 0:
raise UserError(_('No negative quantities allowed'))
elif not ml.is_inventory:
ml_ids_to_delete.add(ml.id)
if ml_ids_tracked_without_lot:
mls_tracked_without_lot = self.env['stock.move.line'].browse(ml_ids_tracked_without_lot)
raise UserError(_('You need to supply a Lot/Serial Number for product: \n - ') +
'\n - '.join(mls_tracked_without_lot.mapped('product_id.display_name')))
ml_to_create_lot = self.env['stock.move.line'].browse(ml_ids_to_create_lot)
ml_to_create_lot._create_and_assign_production_lot()
mls_to_delete = self.env['stock.move.line'].browse(ml_ids_to_delete)
mls_to_delete.unlink()
mls_todo = (self - mls_to_delete)
mls_todo._check_company()
# Now, we can actually move the quant.
ml_ids_to_ignore = OrderedSet()
for ml in mls_todo:
# if this move line is force assigned, unreserve elsewhere if needed
ml._synchronize_quant(-ml.quantity_product_uom, ml.location_id, action="reserved")
available_qty, in_date = ml._synchronize_quant(-ml.quantity_product_uom, ml.location_id)
ml._synchronize_quant(ml.quantity_product_uom, ml.location_dest_id, package=ml.result_package_id, in_date=in_date)
if available_qty < 0:
ml._free_reservation(
ml.product_id, ml.location_id,
abs(available_qty), lot_id=ml.lot_id, package_id=ml.package_id,
owner_id=ml.owner_id, ml_ids_to_ignore=ml_ids_to_ignore)
ml_ids_to_ignore.add(ml.id)
# Reset the reserved quantity as we just moved it to the destination location.
mls_todo.write({
'date': fields.Datetime.now(),
})
def _synchronize_quant(self, quantity, location, action="available", in_date=False, **quants_value):
""" quantity should be express in product's UoM"""
lot = quants_value.get('lot', self.lot_id)
package = quants_value.get('package', self.package_id)
owner = quants_value.get('owner', self.owner_id)
available_qty = 0
if self.product_id.type != 'product' or float_is_zero(quantity, precision_rounding=self.product_uom_id.rounding):
return 0, False
if action == "available":
available_qty, in_date = self.env['stock.quant']._update_available_quantity(self.product_id, location, quantity, lot_id=lot, package_id=package, owner_id=owner, in_date=in_date)
elif action == "reserved" and not self.move_id._should_bypass_reservation():
self.env['stock.quant']._update_reserved_quantity(self.product_id, location, quantity, lot_id=lot, package_id=package, owner_id=owner)
if available_qty < 0 and lot:
# see if we can compensate the negative quants with some untracked quants
untracked_qty = self.env['stock.quant']._get_available_quantity(self.product_id, location, lot_id=False, package_id=package, owner_id=owner, strict=True)
if not untracked_qty:
return available_qty, in_date
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
self.env['stock.quant']._update_available_quantity(self.product_id, location, -taken_from_untracked_qty, lot_id=False, package_id=package, owner_id=owner, in_date=in_date)
self.env['stock.quant']._update_available_quantity(self.product_id, location, taken_from_untracked_qty, lot_id=lot, package_id=package, owner_id=owner, in_date=in_date)
return available_qty, in_date
def _get_similar_move_lines(self):
self.ensure_one()
lines = self.env['stock.move.line']
picking_id = self.move_id.picking_id if self.move_id else self.picking_id
if picking_id:
lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name))
return lines
def _prepare_new_lot_vals(self):
self.ensure_one()
return {
'name': self.lot_name,
'product_id': self.product_id.id,
'company_id': self.company_id.id,
}
def _create_and_assign_production_lot(self):
""" Creates and assign new production lots for move lines."""
lot_vals = []
# It is possible to have multiple time the same lot to create & assign,
# so we handle the case with 2 dictionaries.
key_to_index = {} # key to index of the lot
key_to_mls = defaultdict(lambda: self.env['stock.move.line']) # key to all mls
for ml in self:
key = (ml.company_id.id, ml.product_id.id, ml.lot_name)
key_to_mls[key] |= ml
if ml.tracking != 'lot' or key not in key_to_index:
key_to_index[key] = len(lot_vals)
lot_vals.append(ml._prepare_new_lot_vals())
lots = self.env['stock.lot'].create(lot_vals)
for key, mls in key_to_mls.items():
lot = lots[key_to_index[key]].with_prefetch(lots._ids) # With prefetch to reconstruct the ones broke by accessing by index
mls.write({'lot_id': lot.id})
def _reservation_is_updatable(self, quantity, reserved_quant):
self.ensure_one()
if (self.product_id.tracking != 'serial' and
self.location_id.id == reserved_quant.location_id.id and
self.lot_id.id == reserved_quant.lot_id.id and
self.package_id.id == reserved_quant.package_id.id and
self.owner_id.id == reserved_quant.owner_id.id and
not self.result_package_id):
return True
return False
def _log_message(self, record, move, template, vals):
data = vals.copy()
if 'lot_id' in vals and vals['lot_id'] != move.lot_id.id:
data['lot_name'] = self.env['stock.lot'].browse(vals.get('lot_id')).name
if 'location_id' in vals:
data['location_name'] = self.env['stock.location'].browse(vals.get('location_id')).name
if 'location_dest_id' in vals:
data['location_dest_name'] = self.env['stock.location'].browse(vals.get('location_dest_id')).name
if 'package_id' in vals and vals['package_id'] != move.package_id.id:
data['package_name'] = self.env['stock.quant.package'].browse(vals.get('package_id')).name
if 'package_result_id' in vals and vals['package_result_id'] != move.package_result_id.id:
data['result_package_name'] = self.env['stock.quant.package'].browse(vals.get('result_package_id')).name
if 'owner_id' in vals and vals['owner_id'] != move.owner_id.id:
data['owner_name'] = self.env['res.partner'].browse(vals.get('owner_id')).name
record.message_post_with_source(
template,
render_values={'move': move, 'vals': dict(vals, **data)},
subtype_xmlid='mail.mt_note',
)
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, ml_ids_to_ignore=None):
""" When editing a done move line or validating one with some forced quantities, it is
possible to impact quants that were not reserved. It is therefore necessary to edit or
unlink the move lines that reserved a quantity now unavailable.
:param ml_ids_to_ignore: OrderedSet of `stock.move.line` ids that should NOT be unreserved
"""
self.ensure_one()
if ml_ids_to_ignore is None:
ml_ids_to_ignore = OrderedSet()
ml_ids_to_ignore |= self.ids
if self.move_id._should_bypass_reservation(location_id):
return
# We now have to find the move lines that reserved our now unavailable quantity. We
# take care to exclude ourselves and the move lines were work had already been done.
outdated_move_lines_domain = [
('state', 'not in', ['done', 'cancel']),
('product_id', '=', product_id.id),
('lot_id', '=', lot_id.id if lot_id else False),
('location_id', '=', location_id.id),
('owner_id', '=', owner_id.id if owner_id else False),
('package_id', '=', package_id.id if package_id else False),
('quantity_product_uom', '>', 0.0),
('picked', '=', False),
('id', 'not in', tuple(ml_ids_to_ignore)),
]
# We take the current picking first, then the pickings with the latest scheduled date
def current_picking_first(cand):
return (
cand.picking_id != self.move_id.picking_id,
-(cand.picking_id.scheduled_date or cand.move_id.date).timestamp()
if cand.picking_id or cand.move_id
else -cand.id)
outdated_candidates = self.env['stock.move.line'].search(outdated_move_lines_domain).sorted(current_picking_first)
# As the move's state is not computed over the move lines, we'll have to manually
# recompute the moves which we adapted their lines.
move_to_reassign = self.env['stock.move']
to_unlink_candidate_ids = set()
rounding = self.product_uom_id.rounding
for candidate in outdated_candidates:
move_to_reassign |= candidate.move_id
if float_compare(candidate.quantity_product_uom, quantity, precision_rounding=rounding) <= 0:
quantity -= candidate.quantity_product_uom
to_unlink_candidate_ids.add(candidate.id)
if float_is_zero(quantity, precision_rounding=rounding):
break
else:
candidate.quantity -= candidate.product_id.uom_id._compute_quantity(quantity, candidate.product_uom_id, rounding_method='HALF-UP')
break
self.env['stock.move.line'].browse(to_unlink_candidate_ids).unlink()
move_to_reassign._action_assign()
def _get_aggregated_product_quantities(self, **kwargs):
""" Returns a dictionary of products (key = id+name+description+uom+packaging) and corresponding values of interest.
Allows aggregation of data across separate move lines for the same product. This is expected to be useful
in things such as delivery reports. Dict key is made as a combination of values we expect to want to group
the products by (i.e. so data is not lost). This function purposely ignores lots/SNs because these are
expected to already be properly grouped by line.
returns: dictionary {product_id+name+description+uom+packaging: {product, name, description, quantity, product_uom, packaging}, ...}
"""
aggregated_move_lines = {}
def get_aggregated_properties(move_line=False, move=False):
move = move or move_line.move_id
uom = move.product_uom or move_line.product_uom_id
name = move.product_id.display_name
description = move.description_picking
if description == name or description == move.product_id.name:
description = False
product = move.product_id
line_key = f'{product.id}_{product.display_name}_{description or ""}_{uom.id}_{move.product_packaging_id or ""}'
return (line_key, name, description, uom, move.product_packaging_id)
def _compute_packaging_qtys(aggregated_move_lines):
# Needs to be computed after aggregation of line qtys
for line in aggregated_move_lines.values():
if line['packaging']:
line['packaging_qty'] = line['packaging']._compute_qty(line['qty_ordered'], line['product_uom'])
line['packaging_quantity'] = line['packaging']._compute_qty(line['quantity'], line['product_uom'])
return aggregated_move_lines
# Loops to get backorders, backorders' backorders, and so and so...
backorders = self.env['stock.picking']
pickings = self.picking_id
while pickings.backorder_ids:
backorders |= pickings.backorder_ids
pickings = pickings.backorder_ids
for move_line in self:
if kwargs.get('except_package') and move_line.result_package_id:
continue
line_key, name, description, uom, packaging = get_aggregated_properties(move_line=move_line)
quantity = move_line.product_uom_id._compute_quantity(move_line.quantity, uom)
if line_key not in aggregated_move_lines:
qty_ordered = None
if backorders and not kwargs.get('strict'):
qty_ordered = move_line.move_id.product_uom_qty
# Filters on the aggregation key (product, description and uom) to add the
# quantities delayed to backorders to retrieve the original ordered qty.
following_move_lines = backorders.move_line_ids.filtered(
lambda ml: get_aggregated_properties(move=ml.move_id)[0] == line_key
)
qty_ordered += sum(following_move_lines.move_id.mapped('product_uom_qty'))
# Remove the done quantities of the other move lines of the stock move
previous_move_lines = move_line.move_id.move_line_ids.filtered(
lambda ml: get_aggregated_properties(move=ml.move_id)[0] == line_key and ml.id != move_line.id
)
qty_ordered -= sum([m.product_uom_id._compute_quantity(m.quantity, uom) for m in previous_move_lines])
aggregated_move_lines[line_key] = {
'name': name,
'description': description,
'quantity': quantity,
'qty_ordered': qty_ordered or quantity,
'product_uom': uom,
'product': move_line.product_id,
'packaging': packaging,
}
else:
aggregated_move_lines[line_key]['qty_ordered'] += quantity
aggregated_move_lines[line_key]['quantity'] += quantity
# Does the same for empty move line to retrieve the ordered qty. for partially done moves
# (as they are splitted when the transfer is done and empty moves don't have move lines).
if kwargs.get('strict'):
return _compute_packaging_qtys(aggregated_move_lines)
pickings = (self.picking_id | backorders)
for empty_move in pickings.move_ids:
if not (empty_move.state == "cancel" and empty_move.product_uom_qty
and float_is_zero(empty_move.quantity, precision_rounding=empty_move.product_uom.rounding)):
continue
line_key, name, description, uom, packaging = get_aggregated_properties(move=empty_move)
if line_key not in aggregated_move_lines:
qty_ordered = empty_move.product_uom_qty
aggregated_move_lines[line_key] = {
'name': name,
'description': description,
'quantity': False,
'qty_ordered': qty_ordered,
'product_uom': uom,
'product': empty_move.product_id,
'packaging': packaging,
}
else:
aggregated_move_lines[line_key]['qty_ordered'] += empty_move.product_uom_qty
return _compute_packaging_qtys(aggregated_move_lines)
def _compute_sale_price(self):
# To Override
pass
@api.model
def _prepare_stock_move_vals(self):
self.ensure_one()
return {
'name': _('New Move:') + self.product_id.display_name,
'product_id': self.product_id.id,
'product_uom_qty': 0 if self.picking_id and self.picking_id.state != 'done' else self.quantity,
'product_uom': self.product_uom_id.id,
'description_picking': self.description_picking,
'location_id': self.picking_id.location_id.id,
'location_dest_id': self.picking_id.location_dest_id.id,
'picked': self.picked,
'picking_id': self.picking_id.id,
'state': self.picking_id.state,
'picking_type_id': self.picking_id.picking_type_id.id,
'restrict_partner_id': self.picking_id.owner_id.id,
'company_id': self.picking_id.company_id.id,
'partner_id': self.picking_id.partner_id.id,
'package_level_id': self.package_level_id.id,
}
def _copy_quant_info(self, vals):
quant = self.env['stock.quant'].browse(vals.get('quant_id', 0))
line_data = {
'product_id': quant.product_id.id,
'lot_id': quant.lot_id.id,
'package_id': quant.package_id.id,
'location_id': quant.location_id.id,
'owner_id': quant.owner_id.id,
}
return line_data
def action_open_reference(self):
self.ensure_one()
if self.move_id:
action = self.move_id.action_open_reference()
if action['res_model'] != 'stock.move':
return action
return {
'res_model': self._name,
'type': 'ir.actions.act_window',
'views': [[False, "form"]],
'res_id': self.id,
}
def action_put_in_pack(self):
for picking in self.picking_id:
picking.action_put_in_pack()
return self.picking_id.action_detailed_operations()
def _get_revert_inventory_move_values(self):
self.ensure_one()
return {
'name':_('%s [reverted]', self.reference),
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': self.quantity,
'company_id': self.company_id.id or self.env.company.id,
'state': 'confirmed',
'location_id': self.location_dest_id.id,
'location_dest_id': self.location_id.id,
'is_inventory': True,
'picked': True,
'move_line_ids': [(0, 0, {
'product_id': self.product_id.id,
'product_uom_id': self.product_uom_id.id,
'quantity': self.quantity,
'location_id': self.location_dest_id.id,
'location_dest_id': self.location_id.id,
'company_id': self.company_id.id or self.env.company.id,
'lot_id': self.lot_id.id,
'package_id': self.package_id.id,
'result_package_id': self.package_id.id,
'owner_id': self.owner_id.id,
})]
}
def action_revert_inventory(self):
move_vals = []
# remove inventory mode
self = self.with_context(inventory_mode=False)
processed_move_line = self.env['stock.move.line']
for move_line in self:
if move_line.is_inventory and not float_is_zero(move_line.quantity, precision_digits=move_line.product_uom_id.rounding):
processed_move_line += move_line
move_vals.append(move_line._get_revert_inventory_move_values())
if not processed_move_line:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'danger',
'message': _("There are no inventory adjustments to revert."),
}
}
moves = self.env['stock.move'].create(move_vals)
moves._action_done()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _("The inventory adjustments have been reverted."),
}
}

591
models/stock_orderpoint.py Normal file
View File

@ -0,0 +1,591 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from pytz import timezone, UTC
from collections import defaultdict
from datetime import datetime, time
from dateutil import relativedelta
from psycopg2 import OperationalError
from odoo import SUPERUSER_ID, _, api, fields, models, registry
from odoo.addons.stock.models.stock_rule import ProcurementException
from odoo.exceptions import RedirectWarning, UserError, ValidationError
from odoo.osv import expression
from odoo.tools import float_compare, float_is_zero, frozendict, split_every
_logger = logging.getLogger(__name__)
class StockWarehouseOrderpoint(models.Model):
""" Defines Minimum stock rules. """
_name = "stock.warehouse.orderpoint"
_description = "Minimum Inventory Rule"
_check_company_auto = True
_order = "location_id,company_id,id"
name = fields.Char(
'Name', copy=False, required=True, readonly=True,
default=lambda self: self.env['ir.sequence'].next_by_code('stock.orderpoint'))
trigger = fields.Selection([
('auto', 'Auto'), ('manual', 'Manual')], string='Trigger', default='auto', required=True)
active = fields.Boolean(
'Active', default=True,
help="If the active field is set to False, it will allow you to hide the orderpoint without removing it.")
snoozed_until = fields.Date('Snoozed', help="Hidden until next scheduler.")
warehouse_id = fields.Many2one(
'stock.warehouse', 'Warehouse',
compute="_compute_warehouse_id", store=True, readonly=False, precompute=True,
check_company=True, ondelete="cascade", required=True)
location_id = fields.Many2one(
'stock.location', 'Location', index=True,
compute="_compute_location_id", store=True, readonly=False, precompute=True,
ondelete="cascade", required=True, check_company=True)
product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id')
product_id = fields.Many2one(
'product.product', 'Product',
domain=("[('product_tmpl_id', '=', context.get('active_id', False))] if context.get('active_model') == 'product.template' else"
" [('id', '=', context.get('default_product_id', False))] if context.get('default_product_id') else"
" [('type', '=', 'product')]"),
ondelete='cascade', required=True, check_company=True)
product_category_id = fields.Many2one('product.category', name='Product Category', related='product_id.categ_id', store=True)
product_uom = fields.Many2one(
'uom.uom', 'Unit of Measure', related='product_id.uom_id')
product_uom_name = fields.Char(string='Product unit of measure label', related='product_uom.display_name', readonly=True)
product_min_qty = fields.Float(
'Min Quantity', digits='Product Unit of Measure', required=True, default=0.0,
help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates "
"a procurement to bring the forecasted quantity to the Max Quantity.")
product_max_qty = fields.Float(
'Max Quantity', digits='Product Unit of Measure', required=True, default=0.0,
help="When the virtual stock goes below the Min Quantity, Odoo generates "
"a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity.")
qty_multiple = fields.Float(
'Multiple Quantity', digits='Product Unit of Measure',
default=1, required=True,
help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.")
group_id = fields.Many2one(
'procurement.group', 'Procurement Group', copy=False,
help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by stock rules will be grouped into one big picking.")
company_id = fields.Many2one(
'res.company', 'Company', required=True, index=True,
default=lambda self: self.env.company)
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')
lead_days_date = fields.Date(compute='_compute_lead_days')
route_id = fields.Many2one(
'stock.route', string='Route', domain="[('product_selectable', '=', True)]")
qty_on_hand = fields.Float('On Hand', readonly=True, compute='_compute_qty', digits='Product Unit of Measure')
qty_forecast = fields.Float('Forecast', readonly=True, compute='_compute_qty', digits='Product Unit of Measure')
qty_to_order = fields.Float('To Order', compute='_compute_qty_to_order', store=True, readonly=False, digits='Product Unit of Measure')
#TODO: remove this field in master
days_to_order = fields.Float(compute='_compute_days_to_order', help="Numbers of days in advance that replenishments demands are created.")
visibility_days = fields.Float(
compute='_compute_visibility_days', inverse='_set_visibility_days', readonly=False,
help="Consider product forecast these many days in the future upon product replenishment, set to 0 for just-in-time.\n"
"The value depends on the type of the route (Buy or Manufacture)")
unwanted_replenish = fields.Boolean('Unwanted Replenish', compute="_compute_unwanted_replenish")
_sql_constraints = [
('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
('product_location_check', 'unique (product_id, location_id, company_id)', 'A replenishment rule already exists for this product on this location.'),
]
@api.depends('warehouse_id')
def _compute_allowed_location_ids(self):
loc_domain = [('usage', 'in', ('internal', 'view'))]
# We want to keep only the locations
# - strictly belonging to our warehouse
# - not belonging to any warehouses
for orderpoint in self:
other_warehouses = self.env['stock.warehouse'].search([('id', '!=', orderpoint.warehouse_id.id)])
for view_location_id in other_warehouses.mapped('view_location_id'):
loc_domain = expression.AND([loc_domain, ['!', ('id', 'child_of', view_location_id.id)]])
loc_domain = expression.AND([loc_domain, ['|', ('company_id', '=', False), ('company_id', '=', orderpoint.company_id.id)]])
orderpoint.allowed_location_ids = self.env['stock.location'].search(loc_domain)
@api.depends('rule_ids', 'product_id.seller_ids', 'product_id.seller_ids.delay')
def _compute_lead_days(self):
for orderpoint in self.with_context(bypass_delay_description=True):
if not orderpoint.product_id or not orderpoint.location_id:
orderpoint.lead_days_date = False
continue
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['total_delay'])
orderpoint.lead_days_date = lead_days_date
@api.depends('route_id', 'product_id', 'location_id', 'company_id', 'warehouse_id', 'product_id.route_ids')
def _compute_rules(self):
for orderpoint in self:
if not orderpoint.product_id or not orderpoint.location_id:
orderpoint.rule_ids = False
continue
orderpoint.rule_ids = orderpoint.product_id._get_rules_from_location(orderpoint.location_id, route_ids=orderpoint.route_id)
@api.depends('route_id', 'product_id')
def _compute_visibility_days(self):
self.visibility_days = 0
def _set_visibility_days(self):
return True
@api.depends('route_id', 'product_id')
def _compute_days_to_order(self):
self.days_to_order = 0
@api.constrains('product_id')
def _check_product_uom(self):
''' Check if the UoM has the same category as the product standard UoM '''
if any(orderpoint.product_id.uom_id.category_id != orderpoint.product_uom.category_id for orderpoint in self):
raise ValidationError(_('You have to select a product unit of measure that is in the same category as the default unit of measure of the product'))
@api.depends('location_id', 'company_id')
def _compute_warehouse_id(self):
for orderpoint in self:
if orderpoint.location_id.warehouse_id:
orderpoint.warehouse_id = orderpoint.location_id.warehouse_id
elif orderpoint.company_id:
orderpoint.warehouse_id = orderpoint.env['stock.warehouse'].search([
('company_id', '=', orderpoint.company_id.id)
], limit=1)
@api.depends('warehouse_id', 'company_id')
def _compute_location_id(self):
""" Finds location id for changed warehouse. """
for orderpoint in self:
warehouse = orderpoint.warehouse_id
if not warehouse:
warehouse = orderpoint.env['stock.warehouse'].search([
('company_id', '=', orderpoint.company_id.id)
], limit=1)
orderpoint.location_id = warehouse.lot_stock_id.id
@api.depends('product_id', 'qty_to_order', 'product_max_qty')
def _compute_unwanted_replenish(self):
for orderpoint in self:
if not orderpoint.product_id or float_is_zero(orderpoint.qty_to_order, precision_rounding=orderpoint.product_uom.rounding) or float_compare(orderpoint.product_max_qty, 0, precision_rounding=orderpoint.product_uom.rounding) == -1:
orderpoint.unwanted_replenish = False
else:
after_replenish_qty = orderpoint.product_id.with_context(company_id=orderpoint.company_id.id, location=orderpoint.location_id.id).virtual_available + orderpoint.qty_to_order
orderpoint.unwanted_replenish = float_compare(after_replenish_qty, orderpoint.product_max_qty, precision_rounding=orderpoint.product_uom.rounding) > 0
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.product_uom = self.product_id.uom_id.id
@api.onchange('route_id')
def _onchange_route_id(self):
if self.route_id:
self.qty_multiple = self._get_qty_multiple_to_order()
def write(self, vals):
if 'company_id' in vals:
for orderpoint in self:
if orderpoint.company_id.id != vals['company_id']:
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, force_to_max=False):
now = self.env.cr.now()
if force_to_max:
for orderpoint in self:
orderpoint.qty_to_order = orderpoint.product_max_qty - orderpoint.qty_forecast
remainder = orderpoint.qty_multiple > 0 and orderpoint.qty_to_order % orderpoint.qty_multiple or 0.0
if not float_is_zero(remainder, precision_rounding=orderpoint.product_uom.rounding):
orderpoint.qty_to_order += orderpoint.qty_multiple - remainder
try:
self._procure_orderpoint_confirm(company_id=self.env.company)
except UserError as e:
if len(self) != 1:
raise e
raise RedirectWarning(e, {
'name': self.product_id.display_name,
'type': 'ir.actions.act_window',
'res_model': 'product.product',
'res_id': self.product_id.id,
'views': [(self.env.ref('product.product_normal_form_view').id, 'form')],
}, _('Edit Product'))
notification = False
if len(self) == 1:
notification = self.with_context(written_after=now)._get_replenishment_order_notification()
# Forced to call compute quantity because we don't have a link.
self._compute_qty()
self.filtered(lambda o: o.create_uid.id == SUPERUSER_ID and o.qty_to_order <= 0.0 and o.trigger == 'manual').unlink()
return notification
def action_replenish_auto(self):
self.trigger = 'auto'
return self.action_replenish()
@api.depends('product_id', 'location_id', 'product_id.stock_move_ids', 'product_id.stock_move_ids.state',
'product_id.stock_move_ids.date', 'product_id.stock_move_ids.product_uom_qty')
def _compute_qty(self):
orderpoints_contexts = defaultdict(lambda: self.env['stock.warehouse.orderpoint'])
for orderpoint in self:
if not orderpoint.product_id or not orderpoint.location_id:
orderpoint.qty_on_hand = False
orderpoint.qty_forecast = False
continue
orderpoint_context = orderpoint._get_product_context()
product_context = frozendict({**orderpoint_context})
orderpoints_contexts[product_context] |= orderpoint
for orderpoint_context, orderpoints_by_context in orderpoints_contexts.items():
products_qty = {
p['id']: p for p in orderpoints_by_context.product_id.with_context(orderpoint_context).read(['qty_available', 'virtual_available'])
}
products_qty_in_progress = orderpoints_by_context._quantity_in_progress()
for orderpoint in orderpoints_by_context:
orderpoint.qty_on_hand = products_qty[orderpoint.product_id.id]['qty_available']
orderpoint.qty_forecast = products_qty[orderpoint.product_id.id]['virtual_available'] + products_qty_in_progress[orderpoint.id]
@api.depends('qty_multiple', 'qty_forecast', 'product_min_qty', 'product_max_qty', 'visibility_days')
def _compute_qty_to_order(self):
for orderpoint in self:
if not orderpoint.product_id or not orderpoint.location_id:
orderpoint.qty_to_order = False
continue
qty_to_order = 0.0
rounding = orderpoint.product_uom.rounding
# We want to know how much we should order to also satisfy the needs that gonna appear in the next (visibility) days
product_context = orderpoint._get_product_context(visibility_days=orderpoint.visibility_days)
qty_forecast_with_visibility = orderpoint.product_id.with_context(product_context).read(['virtual_available'])[0]['virtual_available'] + orderpoint._quantity_in_progress()[orderpoint.id]
if float_compare(qty_forecast_with_visibility, orderpoint.product_min_qty, precision_rounding=rounding) < 0:
qty_to_order = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - qty_forecast_with_visibility
remainder = orderpoint.qty_multiple > 0.0 and qty_to_order % orderpoint.qty_multiple or 0.0
if (float_compare(remainder, 0.0, precision_rounding=rounding) > 0
and float_compare(orderpoint.qty_multiple - remainder, 0.0, precision_rounding=rounding) > 0):
qty_to_order += orderpoint.qty_multiple - remainder
orderpoint.qty_to_order = qty_to_order
def _get_qty_multiple_to_order(self):
""" Calculates the minimum quantity that can be ordered according to the PO UoM or BoM
"""
self.ensure_one()
return 0
def _set_default_route_id(self):
""" Write the `route_id` field on `self`. This method is intendend to be called on the
orderpoints generated when openning the replenish report.
"""
self = self.filtered(lambda o: not o.route_id)
rules_groups = self.env['stock.rule']._read_group([
('route_id.product_selectable', '!=', False),
('location_dest_id', 'in', self.location_id.ids),
('action', 'in', ['pull_push', 'pull']),
('route_id.active', '!=', False)
], ['location_dest_id', 'route_id'])
for location_dest, route in rules_groups:
orderpoints = self.filtered(lambda o: o.location_id.id == location_dest.id)
orderpoints.route_id = route
def _get_lead_days_values(self):
self.ensure_one()
return {
'days_to_order': self.days_to_order,
}
def _get_product_context(self, visibility_days=0):
"""Used to call `virtual_available` when running an orderpoint."""
self.ensure_one()
return {
'location': self.location_id.id,
'to_date': datetime.combine(self.lead_days_date + relativedelta.relativedelta(days=visibility_days), time.max)
}
def _get_orderpoint_action(self):
"""Create manual orderpoints for missing product in each warehouses. It also removes
orderpoints that have been replenish. In order to do it:
- It uses the report.stock.quantity to find missing quantity per product/warehouse
- It checks if orderpoint already exist to refill this location.
- It checks if it exists other sources (e.g RFQ) tha refill the warehouse.
- It creates the orderpoints for missing quantity that were not refill by an upper option.
return replenish report ir.actions.act_window
"""
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_orderpoint_replenish")
action['context'] = self.env.context
# Search also with archived ones to avoid to trigger product_location_check SQL constraints later
# It means that when there will be a archived orderpoint on a location + product, the replenishment
# report won't take in account this location + product and it won't create any manual orderpoint
# In master: the active field should be remove
orderpoints = self.env['stock.warehouse.orderpoint'].with_context(active_test=False).search([])
# Remove previous automatically created orderpoint that has been refilled.
orderpoints_removed = orderpoints._unlink_processed_orderpoints()
orderpoints = orderpoints - orderpoints_removed
to_refill = defaultdict(float)
all_product_ids = self._get_orderpoint_products()
all_replenish_location_ids = self._get_orderpoint_locations()
ploc_per_day = defaultdict(set)
# For each replenish location get products with negative virtual_available aka forecast
for loc in all_replenish_location_ids:
for product in all_product_ids.with_context(location=loc.id):
if float_compare(product.virtual_available, 0, precision_rounding=product.uom_id.rounding) >= 0:
continue
# group product by lead_days and location in order to read virtual_available
# in batch
rules = product._get_rules_from_location(loc)
lead_days = rules.with_context(bypass_delay_description=True)._get_lead_days(product)[0]['total_delay']
ploc_per_day[(lead_days, loc)].add(product.id)
# recompute virtual_available with lead days
today = fields.datetime.now().replace(hour=23, minute=59, second=59)
for (days, loc), product_ids in ploc_per_day.items():
products = self.env['product.product'].browse(product_ids)
qties = products.with_context(
location=loc.id,
to_date=today + relativedelta.relativedelta(days=days)
).read(['virtual_available'])
for (product, qty) in zip(products, qties):
if float_compare(qty['virtual_available'], 0, precision_rounding=product.uom_id.rounding) < 0:
to_refill[(qty['id'], loc.id)] = qty['virtual_available']
products.invalidate_recordset()
if not to_refill:
return action
# Remove incoming quantity from other origin than moves (e.g RFQ)
product_ids, location_ids = zip(*to_refill)
qty_by_product_loc, dummy = self.env['product.product'].browse(product_ids)._get_quantity_in_progress(location_ids=location_ids)
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
# Group orderpoint by product-location
orderpoint_by_product_location = self.env['stock.warehouse.orderpoint']._read_group(
[('id', 'in', orderpoints.ids)],
['product_id', 'location_id'],
['qty_to_order:sum'])
orderpoint_by_product_location = {
(product.id, location.id): qty_to_order_sum
for product, location, qty_to_order_sum in orderpoint_by_product_location
}
for (product, location), product_qty in to_refill.items():
qty_in_progress = qty_by_product_loc.get((product, location)) or 0.0
qty_in_progress += orderpoint_by_product_location.get((product, location), 0.0)
# Add qty to order for other orderpoint under this location.
if not qty_in_progress:
continue
to_refill[(product, location)] = product_qty + qty_in_progress
to_refill = {k: v for k, v in to_refill.items() if float_compare(
v, 0.0, precision_digits=rounding) < 0.0}
# With archived ones to avoid `product_location_check` SQL constraints
orderpoint_by_product_location = self.env['stock.warehouse.orderpoint'].with_context(active_test=False)._read_group(
[('id', 'in', orderpoints.ids)],
['product_id', 'location_id'],
['id:recordset'])
orderpoint_by_product_location = {
(product.id, location.id): orderpoint
for product, location, orderpoint in orderpoint_by_product_location
}
orderpoint_values_list = []
for (product, location_id), product_qty in to_refill.items():
orderpoint = orderpoint_by_product_location.get((product, location_id))
if orderpoint:
orderpoint.qty_forecast += product_qty
else:
orderpoint_values = self.env['stock.warehouse.orderpoint']._get_orderpoint_values(product, location_id)
location = self.env['stock.location'].browse(location_id)
orderpoint_values.update({
'name': _('Replenishment Report'),
'warehouse_id': location.warehouse_id.id or self.env['stock.warehouse'].search([('company_id', '=', location.company_id.id)], limit=1).id,
'company_id': location.company_id.id,
})
orderpoint_values_list.append(orderpoint_values)
orderpoints = self.env['stock.warehouse.orderpoint'].with_user(SUPERUSER_ID).create(orderpoint_values_list)
for orderpoint in orderpoints:
orderpoint._set_default_route_id()
orderpoint.qty_multiple = orderpoint._get_qty_multiple_to_order()
return action
@api.model
def _get_orderpoint_values(self, product, location):
return {
'product_id': product,
'location_id': location,
'product_max_qty': 0.0,
'product_min_qty': 0.0,
'trigger': 'manual',
}
def _get_replenishment_order_notification(self):
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'))]])
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,
}
}
return False
def _quantity_in_progress(self):
"""Return Quantities that are not yet in virtual stock but should be deduced from orderpoint rule
(example: purchases created from orderpoints)"""
return dict(self.mapped(lambda x: (x.id, 0.0)))
@api.autovacuum
def _unlink_processed_orderpoints(self):
domain = [
('create_uid', '=', SUPERUSER_ID),
('trigger', '=', 'manual'),
('qty_to_order', '<=', 0)
]
if self.ids:
expression.AND([domain, [('ids', 'in', self.ids)]])
orderpoints_to_remove = self.env['stock.warehouse.orderpoint'].with_context(active_test=False).search(domain)
# Remove previous automatically created orderpoint that has been refilled.
orderpoints_to_remove.unlink()
return orderpoints_to_remove
def _prepare_procurement_values(self, date=False, group=False):
""" Prepare specific key for moves or other components that will be created from a stock rule
comming from an orderpoint. This method could be override in order to add other custom key that could
be used in move/po creation.
"""
date_deadline = date or fields.Date.today()
dates_info = self.product_id._get_dates_info(date_deadline, self.location_id, route_ids=self.route_id)
return {
'route_ids': self.route_id,
'date_planned': dates_info['date_planned'],
'date_order': dates_info['date_order'],
'date_deadline': date or False,
'warehouse_id': self.warehouse_id,
'orderpoint_id': self,
'group_id': group or self.group_id,
}
def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=None, raise_user_error=True):
""" Create procurements based on orderpoints.
:param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing
1000 orderpoints.
This is appropriate for batch jobs only.
"""
self = self.with_company(company_id)
for orderpoints_batch_ids in split_every(1000, self.ids):
if use_new_cursor:
cr = registry(self._cr.dbname).cursor()
self = self.with_env(self.env(cr=cr))
try:
orderpoints_batch = self.env['stock.warehouse.orderpoint'].browse(orderpoints_batch_ids)
all_orderpoints_exceptions = []
while orderpoints_batch:
procurements = []
for orderpoint in orderpoints_batch:
origins = orderpoint.env.context.get('origins', {}).get(orderpoint.id, False)
if origins:
origin = '%s - %s' % (orderpoint.display_name, ','.join(origins))
else:
origin = orderpoint.name
if float_compare(orderpoint.qty_to_order, 0.0, precision_rounding=orderpoint.product_uom.rounding) == 1:
date = orderpoint._get_orderpoint_procurement_date()
global_visibility_days = self.env['ir.config_parameter'].sudo().get_param('stock.visibility_days')
if global_visibility_days:
date -= relativedelta.relativedelta(days=int(global_visibility_days))
values = orderpoint._prepare_procurement_values(date=date)
procurements.append(self.env['procurement.group'].Procurement(
orderpoint.product_id, orderpoint.qty_to_order, orderpoint.product_uom,
orderpoint.location_id, orderpoint.name, origin,
orderpoint.company_id, values))
try:
with self.env.cr.savepoint():
self.env['procurement.group'].with_context(from_orderpoint=True).run(procurements, raise_user_error=raise_user_error)
except ProcurementException as errors:
orderpoints_exceptions = []
for procurement, error_msg in errors.procurement_exceptions:
orderpoints_exceptions += [(procurement.values.get('orderpoint_id'), error_msg)]
all_orderpoints_exceptions += orderpoints_exceptions
failed_orderpoints = self.env['stock.warehouse.orderpoint'].concat(*[o[0] for o in orderpoints_exceptions])
if not failed_orderpoints:
_logger.error('Unable to process orderpoints')
break
orderpoints_batch -= failed_orderpoints
except OperationalError:
if use_new_cursor:
cr.rollback()
continue
else:
raise
else:
orderpoints_batch._post_process_scheduler()
break
# Log an activity on product template for failed orderpoints.
for orderpoint, error_msg in all_orderpoints_exceptions:
existing_activity = self.env['mail.activity'].search([
('res_id', '=', orderpoint.product_id.product_tmpl_id.id),
('res_model_id', '=', self.env.ref('product.model_product_template').id),
('note', '=', error_msg)])
if not existing_activity:
orderpoint.product_id.product_tmpl_id.sudo().activity_schedule(
'mail.mail_activity_data_warning',
note=error_msg,
user_id=orderpoint.product_id.responsible_id.id or SUPERUSER_ID,
)
finally:
if use_new_cursor:
try:
cr.commit()
finally:
cr.close()
_logger.info("A batch of %d orderpoints is processed and committed", len(orderpoints_batch_ids))
return {}
def _post_process_scheduler(self):
return True
def _get_orderpoint_procurement_date(self):
return timezone(self.company_id.partner_id.tz or 'UTC').localize(datetime.combine(self.lead_days_date, time(12))).astimezone(UTC).replace(tzinfo=None)
def _get_orderpoint_products(self):
return self.env['product.product'].search([('type', '=', 'product'), ('stock_move_ids', '!=', False)])
def _get_orderpoint_locations(self):
return self.env['stock.location'].search([('replenish_location', '=', True)])

View File

@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _, api, fields, models
from odoo.tools.float_utils import float_is_zero
class StockPackageLevel(models.Model):
_name = 'stock.package_level'
_description = 'Stock Package Level'
_check_company_auto = True
package_id = fields.Many2one(
'stock.quant.package', 'Package', required=True, check_company=True,
domain="[('location_id', 'child_of', parent.location_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
picking_id = fields.Many2one('stock.picking', 'Picking', check_company=True)
move_ids = fields.One2many('stock.move', 'package_level_id')
move_line_ids = fields.One2many('stock.move.line', 'package_level_id')
location_id = fields.Many2one('stock.location', 'From', compute='_compute_location_id', check_company=True)
location_dest_id = fields.Many2one(
'stock.location', 'To', check_company=True,
compute="_compute_location_dest_id", store=True, readonly=False, precompute=True,
domain="[('id', 'child_of', parent.location_dest_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
is_done = fields.Boolean('Done', compute='_compute_is_done', inverse='_set_is_done')
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('assigned', 'Reserved'),
('new', 'New'),
('done', 'Done'),
('cancel', 'Cancelled'),
],string='State', compute='_compute_state')
is_fresh_package = fields.Boolean(compute='_compute_fresh_pack')
picking_type_code = fields.Selection(related='picking_id.picking_type_code')
show_lots_m2o = fields.Boolean(compute='_compute_show_lot')
show_lots_text = fields.Boolean(compute='_compute_show_lot')
company_id = fields.Many2one('res.company', 'Company', required=True, index=True)
@api.depends('move_line_ids', 'move_line_ids.quantity')
def _compute_is_done(self):
for package_level in self:
# If it is an existing package
if package_level.is_fresh_package:
package_level.is_done = True
else:
package_level.is_done = package_level._check_move_lines_map_quant_package(package_level.package_id, only_picked=True)
def _set_is_done(self):
for package_level in self:
if package_level.is_done:
if not package_level.is_fresh_package:
ml_update_dict = defaultdict(float)
package_level.picking_id.move_line_ids.filtered(
lambda ml: not ml.package_level_id and ml.package_id == package_level.package_id
).unlink()
for quant in package_level.package_id.quant_ids:
corresponding_mls = package_level.move_line_ids.filtered(lambda ml: ml.product_id == quant.product_id and ml.lot_id == quant.lot_id)
to_dispatch = quant.quantity
if corresponding_mls:
for ml in corresponding_mls:
qty = min(to_dispatch, ml.move_id.product_qty) if len(corresponding_mls) > 1 else to_dispatch
to_dispatch = to_dispatch - qty
ml_update_dict[ml] += qty
if float_is_zero(to_dispatch, precision_rounding=ml.product_id.uom_id.rounding):
break
else:
corresponding_move = package_level.move_ids.filtered(lambda m: m.product_id == quant.product_id)[:1]
self.env['stock.move.line'].create({
'location_id': package_level.location_id.id,
'location_dest_id': package_level.location_dest_id.id,
'picking_id': package_level.picking_id.id,
'product_id': quant.product_id.id,
'quantity': quant.quantity,
'product_uom_id': quant.product_id.uom_id.id,
'lot_id': quant.lot_id.id,
'package_id': package_level.package_id.id,
'result_package_id': package_level.package_id.id,
'package_level_id': package_level.id,
'move_id': corresponding_move.id,
'owner_id': quant.owner_id.id,
'picked': True,
})
for rec, quant in ml_update_dict.items():
rec.quantity = quant
rec.picked = True
else:
package_level.move_line_ids.unlink()
@api.depends('move_line_ids', 'move_line_ids.package_id', 'move_line_ids.result_package_id')
def _compute_fresh_pack(self):
for package_level in self:
if not package_level.move_line_ids or all(ml.package_id and ml.package_id == ml.result_package_id for ml in package_level.move_line_ids):
package_level.is_fresh_package = False
else:
package_level.is_fresh_package = True
@api.depends('move_ids', 'move_ids.state', 'move_line_ids', 'move_line_ids.state')
def _compute_state(self):
for package_level in self:
if not package_level.move_ids and not package_level.move_line_ids:
package_level.state = 'draft'
elif not package_level.move_line_ids and package_level.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
package_level.state = 'confirmed'
elif package_level.move_line_ids and not package_level.move_line_ids.filtered(lambda ml: ml.state in ('done', 'cancel')):
if package_level.is_fresh_package:
package_level.state = 'new'
elif package_level._check_move_lines_map_quant_package(package_level.package_id):
package_level.state = 'assigned'
else:
package_level.state = 'confirmed'
elif package_level.move_line_ids.filtered(lambda ml: ml.state =='done'):
package_level.state = 'done'
elif package_level.move_line_ids.filtered(lambda ml: ml.state == 'cancel') or package_level.move_ids.filtered(lambda m: m.state == 'cancel'):
package_level.state = 'cancel'
else:
package_level.state = 'draft'
def _compute_show_lot(self):
for package_level in self:
if any(ml.product_id.tracking != 'none' for ml in package_level.move_line_ids):
if package_level.picking_id.picking_type_id.use_existing_lots or package_level.state == 'done':
package_level.show_lots_m2o = True
package_level.show_lots_text = False
else:
if self.picking_id.picking_type_id.use_create_lots and package_level.state != 'done':
package_level.show_lots_m2o = False
package_level.show_lots_text = True
else:
package_level.show_lots_m2o = False
package_level.show_lots_text = False
else:
package_level.show_lots_m2o = False
package_level.show_lots_text = False
def _generate_moves(self):
for package_level in self:
if package_level.package_id:
for quant in package_level.package_id.quant_ids:
self.env['stock.move'].create({
'picking_id': package_level.picking_id.id,
'name': quant.product_id.display_name,
'product_id': quant.product_id.id,
'product_uom_qty': quant.quantity,
'product_uom': quant.product_id.uom_id.id,
'location_id': package_level.location_id.id,
'location_dest_id': package_level.location_dest_id.id,
'package_level_id': package_level.id,
'company_id': package_level.company_id.id,
})
@api.model_create_multi
def create(self, vals_list):
package_levels = super().create(vals_list)
for package_level, vals in zip(package_levels, vals_list):
if vals.get('location_dest_id'):
package_level.move_line_ids.write({'location_dest_id': vals['location_dest_id']})
package_level.move_ids.write({'location_dest_id': vals['location_dest_id']})
return package_levels
def write(self, vals):
result = super(StockPackageLevel, self).write(vals)
if vals.get('location_dest_id'):
self.mapped('move_line_ids').write({'location_dest_id': vals['location_dest_id']})
self.mapped('move_ids').write({'location_dest_id': vals['location_dest_id']})
return result
def unlink(self):
self.mapped('move_ids').write({'package_level_id': False})
self.mapped('move_line_ids').write({'result_package_id': False})
return super(StockPackageLevel, self).unlink()
def _check_move_lines_map_quant_package(self, package, only_picked=False):
mls = self.move_line_ids
if only_picked:
mls = mls.filtered(lambda ml: ml.picked)
return package._check_move_lines_map_quant(mls)
@api.depends('package_id', 'state', 'is_fresh_package', 'move_ids', 'move_line_ids')
def _compute_location_id(self):
for pl in self:
if pl.state == 'new' or pl.is_fresh_package:
pl.location_id = False
elif pl.state != 'done' and pl.package_id:
pl.location_id = pl.package_id.location_id
elif pl.state == 'confirmed' and pl.move_ids:
pl.location_id = pl.move_ids[0].location_id
elif pl.state in ('assigned', 'done') and pl.move_line_ids:
pl.location_id = pl.move_line_ids[0].location_id
else:
pl.location_id = pl.picking_id.location_id
@api.depends('picking_id', 'picking_id.location_dest_id')
def _compute_location_dest_id(self):
for pl in self:
pl.location_dest_id = pl.picking_id.location_dest_id
def action_show_package_details(self):
self.ensure_one()
view = self.env.ref('stock.package_level_form_edit_view')
return {
'name': _('Package Content'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'stock.package_level',
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new',
'res_id': self.id,
'flags': {'mode': 'readonly'},
}

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, _
class PackageType(models.Model):
_name = 'stock.package.type'
_description = "Stock package type"
def _get_default_length_uom(self):
return self.env['product.template']._get_length_uom_name_from_ir_config_parameter()
def _get_default_weight_uom(self):
return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
name = fields.Char('Package Type', required=True)
sequence = fields.Integer('Sequence', default=1, help="The first in the sequence is the default one.")
height = fields.Float('Height', help="Packaging Height")
width = fields.Float('Width', help="Packaging Width")
packaging_length = fields.Float('Length', help="Packaging Length")
base_weight = fields.Float(string='Weight', help='Weight of the package type')
max_weight = fields.Float('Max Weight', help='Maximum weight shippable in this packaging')
barcode = fields.Char('Barcode', copy=False)
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', default=_get_default_weight_uom)
length_uom_name = fields.Char(string='Length unit of measure label', compute='_compute_length_uom_name', default=_get_default_length_uom)
company_id = fields.Many2one('res.company', 'Company', index=True)
storage_category_capacity_ids = fields.One2many('stock.storage.category.capacity', 'package_type_id', 'Storage Category Capacity', copy=True)
_sql_constraints = [
('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one package type!"),
('positive_height', 'CHECK(height>=0.0)', 'Height must be positive'),
('positive_width', 'CHECK(width>=0.0)', 'Width must be positive'),
('positive_length', 'CHECK(packaging_length>=0.0)', 'Length must be positive'),
('positive_max_weight', 'CHECK(max_weight>=0.0)', 'Max Weight must be positive'),
]
def _compute_length_uom_name(self):
for package_type in self:
package_type.length_uom_name = self.env['product.template']._get_length_uom_name_from_ir_config_parameter()
def _compute_weight_uom_name(self):
for package_type in self:
package_type.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
def copy(self, default=None):
default = dict(default or {})
default['name'] = _("%s (copy)", self.name)
return super().copy(default)

1695
models/stock_picking.py Normal file

File diff suppressed because it is too large Load Diff

1482
models/stock_quant.py Normal file

File diff suppressed because it is too large Load Diff

603
models/stock_rule.py Normal file
View File

@ -0,0 +1,603 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from collections import defaultdict, namedtuple
from dateutil.relativedelta import relativedelta
from odoo import SUPERUSER_ID, _, api, fields, models, registry
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import float_compare, float_is_zero, html_escape
from odoo.tools.misc import split_every
_logger = logging.getLogger(__name__)
class ProcurementException(Exception):
"""An exception raised by ProcurementGroup `run` containing all the faulty
procurements.
"""
def __init__(self, procurement_exceptions):
""":param procurement_exceptions: a list of tuples containing the faulty
procurement and their error messages
:type procurement_exceptions: list
"""
self.procurement_exceptions = procurement_exceptions
class StockRule(models.Model):
""" A rule describe what a procurement should do; produce, buy, move, ... """
_name = 'stock.rule'
_description = "Stock Rule"
_order = "sequence, id"
_check_company_auto = True
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if 'company_id' in fields_list and not res['company_id']:
res['company_id'] = self.env.company.id
return res
name = fields.Char(
'Name', required=True, translate=True,
help="This field will fill the packing origin and the name of its moves")
active = fields.Boolean(
'Active', default=True,
help="If unchecked, it will allow you to hide the rule without removing it.")
group_propagation_option = fields.Selection([
('none', 'Leave Empty'),
('propagate', 'Propagate'),
('fixed', 'Fixed')], string="Propagation of Procurement Group", default='propagate')
group_id = fields.Many2one('procurement.group', 'Fixed Procurement Group')
action = fields.Selection(
selection=[('pull', 'Pull From'), ('push', 'Push To'), ('pull_push', 'Pull & Push')], string='Action',
required=True, index=True)
sequence = fields.Integer('Sequence', default=20)
company_id = fields.Many2one('res.company', 'Company',
default=lambda self: self.env.company,
domain="[('id', '=?', route_company_id)]")
location_dest_id = fields.Many2one('stock.location', 'Destination Location', required=True, check_company=True, index=True)
location_src_id = fields.Many2one('stock.location', 'Source Location', check_company=True, index=True)
route_id = fields.Many2one('stock.route', 'Route', required=True, ondelete='cascade', index=True)
route_company_id = fields.Many2one(related='route_id.company_id', string='Route Company')
procure_method = fields.Selection([
('make_to_stock', 'Take From Stock'),
('make_to_order', 'Trigger Another Rule'),
('mts_else_mto', 'Take From Stock, if unavailable, Trigger Another Rule')], string='Supply Method', default='make_to_stock', required=True,
help="Take From Stock: the products will be taken from the available stock of the source location.\n"
"Trigger Another Rule: the system will try to find a stock rule to bring the products in the source location. The available stock will be ignored.\n"
"Take From Stock, if Unavailable, Trigger Another Rule: the products will be taken from the available stock of the source location."
"If there is no stock available, the system will try to find a rule to bring the products in the source location.")
route_sequence = fields.Integer('Route Sequence', related='route_id.sequence', store=True, compute_sudo=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type',
required=True, check_company=True,
domain="[('code', '=?', picking_type_code_domain)]")
picking_type_code_domain = fields.Char(compute='_compute_picking_type_code_domain')
delay = fields.Integer('Lead Time', default=0, help="The expected date of the created transfer will be computed based on this lead time.")
partner_address_id = fields.Many2one(
'res.partner', 'Partner Address',
check_company=True,
help="Address where goods should be delivered. Optional.")
propagate_cancel = fields.Boolean(
'Cancel Next Move', default=False,
help="When ticked, if the move created by this rule is cancelled, the next move will be cancelled too.")
propagate_carrier = fields.Boolean(
'Propagation of carrier', default=False,
help="When ticked, carrier of shipment will be propagated.")
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', check_company=True, index=True)
propagate_warehouse_id = fields.Many2one(
'stock.warehouse', 'Warehouse to Propagate',
help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)")
auto = fields.Selection([
('manual', 'Manual Operation'),
('transparent', 'Automatic No Step Added')], string='Automatic Move',
default='manual', required=True,
help="The 'Manual Operation' value will create a stock move after the current one. "
"With 'Automatic No Step Added', the location is replaced in the original move.")
rule_message = fields.Html(compute='_compute_action_message')
def copy(self, default=None):
self.ensure_one()
default = dict(default or {})
if 'name' not in default:
default['name'] = _("%s (copy)", self.name)
return super().copy(default=default)
@api.onchange('picking_type_id')
def _onchange_picking_type(self):
""" Modify locations to the default picking type's locations source and
destination.
Enable the delay alert if the picking type is a delivery
"""
self.location_src_id = self.picking_type_id.default_location_src_id.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id
@api.onchange('route_id', 'company_id')
def _onchange_route(self):
""" Ensure that the rule's company is the same than the route's company. """
if self.route_id.company_id:
self.company_id = self.route_id.company_id
if self.picking_type_id.warehouse_id.company_id != self.route_id.company_id:
self.picking_type_id = False
def _get_message_values(self):
""" Return the source, destination and picking_type applied on a stock
rule. The purpose of this function is to avoid code duplication in
_get_message_dict functions since it often requires those data.
"""
source = self.location_src_id and self.location_src_id.display_name or _('Source Location')
destination = self.location_dest_id and self.location_dest_id.display_name or _('Destination Location')
operation = self.picking_type_id and self.picking_type_id.name or _('Operation Type')
return source, destination, operation
def _get_message_dict(self):
""" Return a dict with the different possible message used for the
rule message. It should return one message for each stock.rule action
(except push and pull). This function is override in mrp and
purchase_stock in order to complete the dictionary.
"""
message_dict = {}
source, destination, operation = self._get_message_values()
if self.action in ('push', 'pull', 'pull_push'):
suffix = ""
if self.procure_method == 'make_to_order' and self.location_src_id:
suffix = _("<br>A need is created in <b>%s</b> and a rule will be triggered to fulfill it.", source)
if self.procure_method == 'mts_else_mto' and self.location_src_id:
suffix = _("<br>If the products are not available in <b>%s</b>, a rule will be triggered to bring products in this location.", source)
message_dict = {
'pull': _('When products are needed in <b>%s</b>, <br/> <b>%s</b> are created from <b>%s</b> to fulfill the need.', destination, operation, source) + suffix,
'push': _('When products arrive in <b>%s</b>, <br/> <b>%s</b> are created to send them in <b>%s</b>.', source, operation, destination)
}
return message_dict
@api.depends('action', 'location_dest_id', 'location_src_id', 'picking_type_id', 'procure_method')
def _compute_action_message(self):
""" Generate dynamicaly a message that describe the rule purpose to the
end user.
"""
action_rules = self.filtered(lambda rule: rule.action)
for rule in action_rules:
message_dict = rule._get_message_dict()
message = message_dict.get(rule.action) and message_dict[rule.action] or ""
if rule.action == 'pull_push':
message = message_dict['pull'] + "<br/><br/>" + message_dict['push']
rule.rule_message = message
(self - action_rules).rule_message = None
@api.depends('action')
def _compute_picking_type_code_domain(self):
self.picking_type_code_domain = False
def _run_push(self, move):
""" Apply a push rule on a move.
If the rule is 'no step added' it will modify the destination location
on the move.
If the rule is 'manual operation' it will generate a new move in order
to complete the section define by the rule.
Care this function is not call by method run. It is called explicitely
in stock_move.py inside the method _push_apply
"""
self.ensure_one()
new_date = fields.Datetime.to_string(move.date + relativedelta(days=self.delay))
if self.auto == 'transparent':
old_dest_location = move.location_dest_id
move.write({'date': new_date, 'location_dest_id': self.location_dest_id.id})
# make sure the location_dest_id is consistent with the move line location dest
if move.move_line_ids:
move.move_line_ids.location_dest_id = move.location_dest_id._get_putaway_strategy(move.product_id) or move.location_dest_id
# avoid looping if a push rule is not well configured; otherwise call again push_apply to see if a next step is defined
if self.location_dest_id != old_dest_location:
# TDE FIXME: should probably be done in the move model IMO
return move._push_apply()[:1]
else:
new_move_vals = self._push_prepare_move_copy_values(move, new_date)
new_move = move.sudo().copy(new_move_vals)
if new_move._should_bypass_reservation():
new_move.write({'procure_method': 'make_to_stock'})
if not new_move.location_id.should_bypass_reservation():
move.write({'move_dest_ids': [(4, new_move.id)]})
return new_move
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
company_id = self.company_id.id
if not company_id:
company_id = self.sudo().warehouse_id and self.sudo().warehouse_id.company_id.id or self.sudo().picking_type_id.warehouse_id.company_id.id
new_move_vals = {
'origin': move_to_copy.origin or move_to_copy.picking_id.name or "/",
'location_id': move_to_copy.location_dest_id.id,
'location_dest_id': self.location_dest_id.id,
'date': new_date,
'date_deadline': move_to_copy.date_deadline,
'company_id': company_id,
'picking_id': False,
'picking_type_id': self.picking_type_id.id,
'propagate_cancel': self.propagate_cancel,
'warehouse_id': self.warehouse_id.id,
'procure_method': 'make_to_order',
}
return new_move_vals
@api.model
def _run_pull(self, procurements):
moves_values_by_company = defaultdict(list)
mtso_products_by_locations = defaultdict(list)
# To handle the `mts_else_mto` procure method, we do a preliminary loop to
# isolate the products we would need to read the forecasted quantity,
# in order to to batch the read. We also make a sanitary check on the
# `location_src_id` field.
for procurement, rule in procurements:
if not rule.location_src_id:
msg = _('No source location defined on stock rule: %s!', rule.name)
raise ProcurementException([(procurement, msg)])
if rule.procure_method == 'mts_else_mto':
mtso_products_by_locations[rule.location_src_id].append(procurement.product_id.id)
# Get the forecasted quantity for the `mts_else_mto` procurement.
forecasted_qties_by_loc = {}
for location, product_ids in mtso_products_by_locations.items():
products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
forecasted_qties_by_loc[location] = {product.id: product.free_qty for product in products}
# Prepare the move values, adapt the `procure_method` if needed.
procurements = sorted(procurements, key=lambda proc: float_compare(proc[0].product_qty, 0.0, precision_rounding=proc[0].product_uom.rounding) > 0)
for procurement, rule in procurements:
procure_method = rule.procure_method
if rule.procure_method == 'mts_else_mto':
qty_needed = procurement.product_uom._compute_quantity(procurement.product_qty, procurement.product_id.uom_id)
if float_compare(qty_needed, 0, precision_rounding=procurement.product_id.uom_id.rounding) <= 0:
procure_method = 'make_to_order'
for move in procurement.values.get('group_id', self.env['procurement.group']).stock_move_ids:
if move.rule_id == rule and float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) > 0:
procure_method = move.procure_method
break
forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed
elif float_compare(qty_needed, forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id],
precision_rounding=procurement.product_id.uom_id.rounding) > 0:
procure_method = 'make_to_order'
else:
forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed
procure_method = 'make_to_stock'
move_values = rule._get_stock_move_values(*procurement)
move_values['procure_method'] = procure_method
moves_values_by_company[procurement.company_id.id].append(move_values)
for company_id, moves_values in moves_values_by_company.items():
# create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
moves = self.env['stock.move'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(moves_values)
# Since action_confirm launch following procurement_group we should activate it.
moves._action_confirm()
return True
def _get_custom_move_fields(self):
""" The purpose of this method is to be override in order to easily add
fields from procurement 'values' argument to move data.
"""
return []
def _get_stock_move_values(self, product_id, product_qty, product_uom, location_dest_id, name, origin, company_id, values):
''' Returns a dictionary of values that will be used to create a stock move from a procurement.
This function assumes that the given procurement has a rule (action == 'pull' or 'pull_push') set on it.
:param procurement: browse record
:rtype: dictionary
'''
group_id = False
if self.group_propagation_option == 'propagate':
group_id = values.get('group_id', False) and values['group_id'].id
elif self.group_propagation_option == 'fixed':
group_id = self.group_id.id
date_scheduled = fields.Datetime.to_string(
fields.Datetime.from_string(values['date_planned']) - relativedelta(days=self.delay or 0)
)
date_deadline = values.get('date_deadline') and (fields.Datetime.to_datetime(values['date_deadline']) - relativedelta(days=self.delay or 0)) or False
partner = self.partner_address_id or (values.get('group_id', False) and values['group_id'].partner_id)
if partner:
product_id = product_id.with_context(lang=partner.lang or self.env.user.lang)
picking_description = product_id._get_description(self.picking_type_id)
if values.get('product_description_variants'):
picking_description += values['product_description_variants']
# it is possible that we've already got some move done, so check for the done qty and create
# a new move with the correct qty
qty_left = product_qty
move_dest_ids = []
if not self.location_dest_id.should_bypass_reservation():
move_dest_ids = values.get('move_dest_ids', False) and [(4, x.id) for x in values['move_dest_ids']] or []
# when create chained moves for inter-warehouse transfers, set the warehouses as partners
if not partner and move_dest_ids:
move_dest = values['move_dest_ids']
if location_dest_id == company_id.internal_transit_location_id:
partners = move_dest.location_dest_id.warehouse_id.partner_id
if len(partners) == 1:
partner = partners
move_dest.partner_id = self.location_src_id.warehouse_id.partner_id or self.company_id.partner_id
move_values = {
'name': name[:2000],
'company_id': self.company_id.id or self.location_src_id.company_id.id or self.location_dest_id.company_id.id or company_id.id,
'product_id': product_id.id,
'product_uom': product_uom.id,
'product_uom_qty': qty_left,
'partner_id': partner.id if partner else False,
'location_id': self.location_src_id.id,
'location_dest_id': location_dest_id.id,
'move_dest_ids': move_dest_ids,
'rule_id': self.id,
'procure_method': self.procure_method,
'origin': origin,
'picking_type_id': self.picking_type_id.id,
'group_id': group_id,
'route_ids': [(4, route.id) for route in values.get('route_ids', [])],
'warehouse_id': self.propagate_warehouse_id.id or self.warehouse_id.id,
'date': date_scheduled,
'date_deadline': False if self.group_propagation_option == 'fixed' else date_deadline,
'propagate_cancel': self.propagate_cancel,
'description_picking': picking_description,
'priority': values.get('priority', "0"),
'orderpoint_id': values.get('orderpoint_id') and values['orderpoint_id'].id,
'product_packaging_id': values.get('product_packaging_id') and values['product_packaging_id'].id,
}
for field in self._get_custom_move_fields():
if field in values:
move_values[field] = values.get(field)
return move_values
def _get_lead_days(self, product, **values):
"""Returns the cumulative delay and its description encountered by a
procurement going through the rules in `self`.
:param product: the product of the procurement
:type product: :class:`~odoo.addons.product.models.product.ProductProduct`
:return: the cumulative delay and cumulative delay's description
:rtype: tuple[defaultdict(float), list[str, str]]
"""
delays = defaultdict(float)
delay = sum(self.filtered(lambda r: r.action in ['pull', 'pull_push']).mapped('delay'))
delays['total_delay'] += delay
global_visibility_days = self.env['ir.config_parameter'].sudo().get_param('stock.visibility_days')
if global_visibility_days:
delays['total_delay'] += int(global_visibility_days)
if self.env.context.get('bypass_delay_description'):
delay_description = []
else:
delay_description = [
(_('Delay on %s', rule.name), _('+ %d day(s)', rule.delay))
for rule in self
if rule.action in ['pull', 'pull_push'] and rule.delay
]
if global_visibility_days:
delay_description.append((_('Global Visibility Days'), _('+ %d day(s)', int(global_visibility_days))))
return delays, delay_description
class ProcurementGroup(models.Model):
"""
The procurement group class is used to group products together
when computing procurements. (tasks, physical products, ...)
The goal is that when you have one sales order of several products
and the products are pulled from the same or several location(s), to keep
having the moves grouped into pickings that represent the sales order.
Used in: sales order (to group delivery order lines like the so), pull/push
rules (to pack like the delivery order), on orderpoints (e.g. for wave picking
all the similar products together).
Grouping is made only if the source and the destination is the same.
Suppose you have 4 lines on a picking from Output where 2 lines will need
to come from Input (crossdock) and 2 lines coming from Stock -> Output As
the four will have the same group ids from the SO, the move from input will
have a stock.picking with 2 grouped lines and the move from stock will have
2 grouped lines also.
The name is usually the name of the original document (sales order) or a
sequence computed if created manually.
"""
_name = 'procurement.group'
_description = 'Procurement Group'
_order = "id desc"
Procurement = namedtuple('Procurement', ['product_id', 'product_qty',
'product_uom', 'location_id', 'name', 'origin', 'company_id', 'values'])
partner_id = fields.Many2one('res.partner', 'Partner')
name = fields.Char(
'Reference',
default=lambda self: self.env['ir.sequence'].next_by_code('procurement.group') or '',
required=True)
move_type = fields.Selection([
('direct', 'Partial'),
('one', 'All at once')], string='Delivery Type', default='direct',
required=True)
stock_move_ids = fields.One2many('stock.move', 'group_id', string="Related Stock Moves")
@api.model
def _skip_procurement(self, procurement):
return procurement.product_id.type not in ("consu", "product") or float_is_zero(
procurement.product_qty, precision_rounding=procurement.product_uom.rounding
)
@api.model
def run(self, procurements, raise_user_error=True):
"""Fulfil `procurements` with the help of stock rules.
Procurements are needs of products at a certain location. To fulfil
these needs, we need to create some sort of documents (`stock.move`
by default, but extensions of `_run_` methods allow to create every
type of documents).
:param procurements: the description of the procurement
:type list: list of `~odoo.addons.stock.models.stock_rule.ProcurementGroup.Procurement`
:param raise_user_error: will raise either an UserError or a ProcurementException
:type raise_user_error: boolan, optional
:raises UserError: if `raise_user_error` is True and a procurement isn't fulfillable
:raises ProcurementException: if `raise_user_error` is False and a procurement isn't fulfillable
"""
def raise_exception(procurement_errors):
if raise_user_error:
dummy, errors = zip(*procurement_errors)
raise UserError('\n'.join(errors))
else:
raise ProcurementException(procurement_errors)
actions_to_run = defaultdict(list)
procurement_errors = []
for procurement in procurements:
procurement.values.setdefault('company_id', procurement.location_id.company_id)
procurement.values.setdefault('priority', '0')
procurement.values.setdefault('date_planned', procurement.values.get('date_planned', False) or fields.Datetime.now())
if self._skip_procurement(procurement):
continue
rule = self._get_rule(procurement.product_id, procurement.location_id, procurement.values)
if not rule:
error = _('No rule has been found to replenish %r in %r.\nVerify the routes configuration on the product.',
procurement.product_id.display_name, procurement.location_id.display_name)
procurement_errors.append((procurement, error))
else:
action = 'pull' if rule.action == 'pull_push' else rule.action
actions_to_run[action].append((procurement, rule))
if procurement_errors:
raise_exception(procurement_errors)
for action, procurements in actions_to_run.items():
if hasattr(self.env['stock.rule'], '_run_%s' % action):
try:
getattr(self.env['stock.rule'], '_run_%s' % action)(procurements)
except ProcurementException as e:
procurement_errors += e.procurement_exceptions
else:
_logger.error("The method _run_%s doesn't exist on the procurement rules" % action)
if procurement_errors:
raise_exception(procurement_errors)
return True
@api.model
def _search_rule(self, route_ids, packaging_id, product_id, warehouse_id, domain):
""" First find a rule among the ones defined on the procurement
group, then try on the routes defined for the product, finally fallback
on the default behavior
"""
if warehouse_id:
domain = expression.AND([['|', ('warehouse_id', '=', warehouse_id.id), ('warehouse_id', '=', False)], domain])
Rule = self.env['stock.rule']
res = self.env['stock.rule']
if route_ids:
res = Rule.search(expression.AND([[('route_id', 'in', route_ids.ids)], domain]), order='route_sequence, sequence', limit=1)
if not res and packaging_id:
packaging_routes = packaging_id.route_ids
if packaging_routes:
res = Rule.search(expression.AND([[('route_id', 'in', packaging_routes.ids)], domain]), order='route_sequence, sequence', limit=1)
if not res:
product_routes = product_id.route_ids | product_id.categ_id.total_route_ids
if product_routes:
res = Rule.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), order='route_sequence, sequence', limit=1)
if not res and warehouse_id:
warehouse_routes = warehouse_id.route_ids
if warehouse_routes:
res = Rule.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), order='route_sequence, sequence', limit=1)
return res
@api.model
def _get_rule(self, product_id, location_id, values):
""" Find a pull rule for the location_id, fallback on the parent
locations if it could not be found.
"""
result = self.env['stock.rule']
location = location_id
while (not result) and location:
domain = self._get_rule_domain(location, values)
result = self._search_rule(values.get('route_ids', False), values.get('product_packaging_id', False), product_id, values.get('warehouse_id', location.warehouse_id), domain)
location = location.location_id
return result
@api.model
def _get_rule_domain(self, location, values):
domain = ['&', ('location_dest_id', '=', location.id), ('action', '!=', 'push')]
# In case the method is called by the superuser, we need to restrict the rules to the
# ones of the company. This is not useful as a regular user since there is a record
# rule to filter out the rules based on the company.
if self.env.su and values.get('company_id'):
domain_company = ['|', ('company_id', '=', False), ('company_id', 'child_of', values['company_id'].ids)]
domain = expression.AND([domain, domain_company])
return domain
@api.model
def _get_moves_to_assign_domain(self, company_id):
moves_domain = [
('state', 'in', ['confirmed', 'partially_available']),
('product_uom_qty', '!=', 0.0),
('reservation_date', '<=', fields.Date.today())
]
if company_id:
moves_domain = expression.AND([[('company_id', '=', company_id)], moves_domain])
return moves_domain
@api.model
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
# Minimum stock rules
domain = self._get_orderpoint_domain(company_id=company_id)
orderpoints = self.env['stock.warehouse.orderpoint'].search(domain)
# ensure that qty_* which depends on datetime.now() are correctly
# recomputed
orderpoints.sudo()._compute_qty_to_order()
if use_new_cursor:
self._cr.commit()
orderpoints.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id, raise_user_error=False)
# Search all confirmed stock_moves and try to assign them
domain = self._get_moves_to_assign_domain(company_id)
moves_to_assign = self.env['stock.move'].search(domain, limit=None,
order='reservation_date, priority desc, date asc, id asc')
for moves_chunk in split_every(1000, moves_to_assign.ids):
self.env['stock.move'].browse(moves_chunk).sudo()._action_assign()
if use_new_cursor:
self._cr.commit()
_logger.info("A batch of %d moves are assigned and committed", len(moves_chunk))
# Merge duplicated quants
self.env['stock.quant']._quant_tasks()
if use_new_cursor:
self._cr.commit()
_logger.info("_run_scheduler_tasks is finished and committed")
@api.model
def run_scheduler(self, use_new_cursor=False, company_id=False):
""" Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules
and the availability of moves. This function is intended to be run for all the companies at the same time, so
we run functions as SUPERUSER to avoid intercompanies and access rights issues. """
try:
if use_new_cursor:
cr = registry(self._cr.dbname).cursor()
self = self.with_env(self.env(cr=cr)) # TDE FIXME
self._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id)
except Exception:
_logger.error("Error during stock scheduler", exc_info=True)
raise
finally:
if use_new_cursor:
try:
self._cr.close()
except Exception:
pass
return {}
@api.model
def _get_orderpoint_domain(self, company_id=False):
domain = [('trigger', '=', 'auto'), ('product_id.active', '=', True)]
if company_id:
domain += [('company_id', '=', company_id)]
return domain

219
models/stock_scrap.py Normal file
View File

@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
from odoo.tools.misc import clean_context
class StockScrap(models.Model):
_name = 'stock.scrap'
_inherit = ['mail.thread']
_order = 'id desc'
_description = 'Scrap'
name = fields.Char(
'Reference', default=lambda self: _('New'),
copy=False, readonly=True, required=True)
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, required=True)
origin = fields.Char(string='Source Document')
product_id = fields.Many2one(
'product.product', 'Product', domain="[('type', 'in', ['product', 'consu'])]",
required=True, check_company=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure',
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
required=True, domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
tracking = fields.Selection(string='Product Tracking', readonly=True, related="product_id.tracking")
lot_id = fields.Many2one(
'stock.lot', 'Lot/Serial',
domain="[('product_id', '=', product_id)]", check_company=True)
package_id = fields.Many2one(
'stock.quant.package', 'Package',
check_company=True)
owner_id = fields.Many2one('res.partner', 'Owner', check_company=True)
move_ids = fields.One2many('stock.move', 'scrap_id')
picking_id = fields.Many2one('stock.picking', 'Picking', check_company=True)
location_id = fields.Many2one(
'stock.location', 'Source Location',
compute='_compute_location_id', store=True, required=True, precompute=True,
domain="[('usage', '=', 'internal')]", check_company=True, readonly=False)
scrap_location_id = fields.Many2one(
'stock.location', 'Scrap Location',
compute='_compute_scrap_location_id', store=True, required=True, precompute=True,
domain="[('scrap_location', '=', True)]", check_company=True, readonly=False)
scrap_qty = fields.Float(
'Quantity', required=True, digits='Product Unit of Measure',
compute='_compute_scrap_qty', default=1.0, readonly=False, store=True)
state = fields.Selection([
('draft', 'Draft'),
('done', 'Done')],
string='Status', default="draft", readonly=True, tracking=True)
date_done = fields.Datetime('Date', readonly=True)
should_replenish = fields.Boolean(string='Replenish Quantities')
@api.depends('product_id')
def _compute_product_uom_id(self):
for scrap in self:
scrap.product_uom_id = scrap.product_id.uom_id
@api.depends('company_id', 'picking_id')
def _compute_location_id(self):
groups = self.env['stock.warehouse']._read_group(
[('company_id', 'in', self.company_id.ids)], ['company_id'], ['lot_stock_id:array_agg'])
locations_per_company = {
company.id: lot_stock_ids[0] if lot_stock_ids else False
for company, lot_stock_ids in groups
}
for scrap in self:
if scrap.picking_id:
scrap.location_id = scrap.picking_id.location_dest_id if scrap.picking_id.state == 'done' else scrap.picking_id.location_id
else:
scrap.location_id = locations_per_company[scrap.company_id.id]
@api.depends('company_id')
def _compute_scrap_location_id(self):
groups = self.env['stock.location']._read_group(
[('company_id', 'in', self.company_id.ids), ('scrap_location', '=', True)], ['company_id'], ['id:min'])
locations_per_company = {
company.id: stock_warehouse_id
for company, stock_warehouse_id in groups
}
for scrap in self:
scrap.scrap_location_id = locations_per_company[scrap.company_id.id]
@api.depends('move_ids', 'move_ids.move_line_ids.quantity', 'product_id')
def _compute_scrap_qty(self):
self.scrap_qty = 1
for scrap in self:
if scrap.move_ids:
scrap.scrap_qty = scrap.move_ids[0].quantity
@api.onchange('lot_id')
def _onchange_serial_number(self):
if self.product_id.tracking == 'serial' and self.lot_id:
message, recommended_location = self.env['stock.quant']._check_serial_number(self.product_id,
self.lot_id,
self.company_id,
self.location_id,
self.picking_id.location_dest_id)
if message:
if recommended_location:
self.location_id = recommended_location
return {'warning': {'title': _('Warning'), 'message': message}}
@api.ondelete(at_uninstall=False)
def _unlink_except_done(self):
if 'done' in self.mapped('state'):
raise UserError(_('You cannot delete a scrap which is done.'))
def _prepare_move_values(self):
self.ensure_one()
return {
'name': self.name,
'origin': self.origin or self.picking_id.name or self.name,
'company_id': self.company_id.id,
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'state': 'draft',
'product_uom_qty': self.scrap_qty,
'location_id': self.location_id.id,
'scrapped': True,
'scrap_id': self.id,
'location_dest_id': self.scrap_location_id.id,
'move_line_ids': [(0, 0, {
'product_id': self.product_id.id,
'product_uom_id': self.product_uom_id.id,
'quantity': self.scrap_qty,
'location_id': self.location_id.id,
'location_dest_id': self.scrap_location_id.id,
'package_id': self.package_id.id,
'owner_id': self.owner_id.id,
'lot_id': self.lot_id.id,
})],
# 'restrict_partner_id': self.owner_id.id,
'picked': True,
'picking_id': self.picking_id.id
}
def do_scrap(self):
self._check_company()
for scrap in self:
scrap.name = self.env['ir.sequence'].next_by_code('stock.scrap') or _('New')
move = self.env['stock.move'].create(scrap._prepare_move_values())
# master: replace context by cancel_backorder
move.with_context(is_scrap=True)._action_done()
scrap.write({'state': 'done'})
scrap.date_done = fields.Datetime.now()
if scrap.should_replenish:
scrap.do_replenish()
return True
def do_replenish(self, values=False):
self.ensure_one()
values = values or {}
self.with_context(clean_context(self.env.context)).env['procurement.group'].run([self.env['procurement.group'].Procurement(
self.product_id,
self.scrap_qty,
self.product_uom_id,
self.location_id,
self.name,
self.name,
self.company_id,
values
)])
def action_get_stock_picking(self):
action = self.env['ir.actions.act_window']._for_xml_id('stock.action_picking_tree_all')
action['domain'] = [('id', '=', self.picking_id.id)]
return action
def action_get_stock_move_lines(self):
action = self.env['ir.actions.act_window']._for_xml_id('stock.stock_move_line_action')
action['domain'] = [('move_id', 'in', self.move_ids.ids)]
return action
def _should_check_available_qty(self):
return self.product_id.type == 'product'
def check_available_qty(self):
if not self._should_check_available_qty():
return True
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
available_qty = self.with_context(
location=self.location_id.id,
lot_id=self.lot_id.id,
package_id=self.package_id.id,
owner_id=self.owner_id.id
).product_id.qty_available
scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id)
return float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0
def action_validate(self):
self.ensure_one()
if float_is_zero(self.scrap_qty,
precision_rounding=self.product_uom_id.rounding):
raise UserError(_('You can only enter positive quantities.'))
if self.check_available_qty():
return self.do_scrap()
else:
ctx = dict(self.env.context)
ctx.update({
'default_product_id': self.product_id.id,
'default_location_id': self.location_id.id,
'default_scrap_id': self.id,
'default_quantity': self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id),
'default_product_uom_name': self.product_id.uom_name
})
return {
'name': self.product_id.display_name + _(': Insufficient Quantity To Scrap'),
'view_mode': 'form',
'res_model': 'stock.warn.insufficient.qty.scrap',
'view_id': self.env.ref('stock.stock_warn_insufficient_qty_scrap_form_view').id,
'type': 'ir.actions.act_window',
'context': ctx,
'target': 'new'
}

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class StorageCategory(models.Model):
_name = 'stock.storage.category'
_description = "Storage Category"
_order = "name"
name = fields.Char('Storage Category', required=True)
max_weight = fields.Float('Max Weight', digits='Stock Weight')
capacity_ids = fields.One2many('stock.storage.category.capacity', 'storage_category_id', copy=True)
product_capacity_ids = fields.One2many('stock.storage.category.capacity', compute="_compute_storage_capacity_ids", inverse="_set_storage_capacity_ids")
package_capacity_ids = fields.One2many('stock.storage.category.capacity', compute="_compute_storage_capacity_ids", inverse="_set_storage_capacity_ids")
allow_new_product = fields.Selection([
('empty', 'If the location is empty'),
('same', 'If all products are same'),
('mixed', 'Allow mixed products')], default='mixed', required=True)
location_ids = fields.One2many('stock.location', 'storage_category_id')
company_id = fields.Many2one('res.company', 'Company')
weight_uom_name = fields.Char(string='Weight unit', compute='_compute_weight_uom_name')
_sql_constraints = [
('positive_max_weight', 'CHECK(max_weight >= 0)', 'Max weight should be a positive number.'),
]
@api.depends('capacity_ids')
def _compute_storage_capacity_ids(self):
for storage_category in self:
storage_category.product_capacity_ids = storage_category.capacity_ids.filtered(lambda c: c.product_id)
storage_category.package_capacity_ids = storage_category.capacity_ids.filtered(lambda c: c.package_type_id)
def _compute_weight_uom_name(self):
self.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
def _set_storage_capacity_ids(self):
for storage_category in self:
storage_category.capacity_ids = storage_category.product_capacity_ids | storage_category.package_capacity_ids
def copy(self, default=None):
default = dict(default or {})
default['name'] = _("%s (copy)", self.name)
return super().copy(default)
class StorageCategoryProductCapacity(models.Model):
_name = 'stock.storage.category.capacity'
_description = "Storage Category Capacity"
_check_company_auto = True
_order = "storage_category_id"
storage_category_id = fields.Many2one('stock.storage.category', ondelete='cascade', required=True, index=True)
product_id = fields.Many2one('product.product', 'Product', ondelete='cascade', check_company=True,
domain=("[('product_tmpl_id', '=', context.get('active_id', False))] if context.get('active_model') == 'product.template' else"
" [('id', '=', context.get('default_product_id', False))] if context.get('default_product_id') else"
" [('type', '=', 'product')]"))
package_type_id = fields.Many2one('stock.package.type', 'Package Type', ondelete='cascade', check_company=True)
quantity = fields.Float('Quantity', required=True)
product_uom_id = fields.Many2one(related='product_id.uom_id')
company_id = fields.Many2one('res.company', 'Company', related="storage_category_id.company_id")
_sql_constraints = [
('positive_quantity', 'CHECK(quantity > 0)', 'Quantity should be a positive number.'),
('unique_product', 'UNIQUE(product_id, storage_category_id)', 'Multiple capacity rules for one product.'),
('unique_package_type', 'UNIQUE(package_type_id, storage_category_id)', 'Multiple capacity rules for one package type.'),
]

1085
models/stock_warehouse.py Normal file

File diff suppressed because it is too large Load Diff

5
populate/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock
from . import product

Some files were not shown because too many files have changed in this diff Show More