Начальное наполнение
This commit is contained in:
parent
8a1014b90b
commit
9f0c6b446c
30
__init__.py
Normal file
30
__init__.py
Normal 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
121
__manifest__.py
Normal 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
1
controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
38
controllers/main.py
Normal file
38
controllers/main.py
Normal 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
|
40
data/default_barcode_patterns.xml
Normal file
40
data/default_barcode_patterns.xml
Normal 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
26
data/digest_data.xml
Normal 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>
|
48
data/mail_template_data.xml
Normal file
48
data/mail_template_data.xml
Normal 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
32
data/mail_templates.xml
Normal 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
130
data/stock_data.xml
Normal 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
214
data/stock_demo.xml
Normal 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
566
data/stock_demo2.xml
Normal 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
132
data/stock_demo_pre.xml
Normal 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>
|
44
data/stock_orderpoint_demo.xml
Normal file
44
data/stock_orderpoint_demo.xml
Normal 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>
|
72
data/stock_sequence_data.xml
Normal file
72
data/stock_sequence_data.xml
Normal 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>
|
29
data/stock_storage_category_demo.xml
Normal file
29
data/stock_storage_category_demo.xml
Normal 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>
|
10
data/stock_traceability_report_data.xml
Normal file
10
data/stock_traceability_report_data.xml
Normal 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&active_model=:active_model', 'model': 'stock.traceability.report'}" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
9547
i18n/af.po
Normal file
9547
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
11163
i18n/ar.po
Normal file
11163
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
9595
i18n/az.po
Normal file
9595
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
10874
i18n/bg.po
Normal file
10874
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
9549
i18n/bs.po
Normal file
9549
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
11240
i18n/ca.po
Normal file
11240
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
11181
i18n/cs.po
Normal file
11181
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
11020
i18n/da.po
Normal file
11020
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
11406
i18n/de.po
Normal file
11406
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
9570
i18n/el.po
Normal file
9570
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
9546
i18n/en_GB.po
Normal file
9546
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
11364
i18n/es.po
Normal file
11364
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
11374
i18n/es_419.po
Normal file
11374
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
9553
i18n/es_BO.po
Normal file
9553
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
9546
i18n/es_CL.po
Normal file
9546
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
9562
i18n/es_CO.po
Normal file
9562
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
9545
i18n/es_CR.po
Normal file
9545
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
9571
i18n/es_DO.po
Normal file
9571
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
9564
i18n/es_EC.po
Normal file
9564
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
9548
i18n/es_PE.po
Normal file
9548
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
9545
i18n/es_VE.po
Normal file
9545
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
11178
i18n/et.po
Normal file
11178
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
11027
i18n/fa.po
Normal file
11027
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
11275
i18n/fi.po
Normal file
11275
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
11377
i18n/fr.po
Normal file
11377
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
9544
i18n/fr_BE.po
Normal file
9544
i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
9545
i18n/gl.po
Normal file
9545
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
9557
i18n/gu.po
Normal file
9557
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
10884
i18n/he.po
Normal file
10884
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
9615
i18n/hr.po
Normal file
9615
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
10854
i18n/hu.po
Normal file
10854
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
11288
i18n/id.po
Normal file
11288
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
9552
i18n/is.po
Normal file
9552
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
11350
i18n/it.po
Normal file
11350
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
10881
i18n/ja.po
Normal file
10881
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
9545
i18n/kab.po
Normal file
9545
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
9549
i18n/km.po
Normal file
9549
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
10940
i18n/ko.po
Normal file
10940
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
9547
i18n/lb.po
Normal file
9547
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
10905
i18n/lt.po
Normal file
10905
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
10705
i18n/lv.po
Normal file
10705
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
9550
i18n/mk.po
Normal file
9550
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
9663
i18n/mn.po
Normal file
9663
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
9585
i18n/nb.po
Normal file
9585
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
11354
i18n/nl.po
Normal file
11354
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
11211
i18n/pl.po
Normal file
11211
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
10855
i18n/pt.po
Normal file
10855
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
11343
i18n/pt_BR.po
Normal file
11343
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
9693
i18n/ro.po
Normal file
9693
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
11345
i18n/ru.po
Normal file
11345
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
10891
i18n/sk.po
Normal file
10891
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
10758
i18n/sl.po
Normal file
10758
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
11175
i18n/sr.po
Normal file
11175
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
9549
i18n/sr@latin.po
Normal file
9549
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
10660
i18n/stock.pot
Normal file
10660
i18n/stock.pot
Normal file
File diff suppressed because it is too large
Load Diff
11224
i18n/sv.po
Normal file
11224
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
11173
i18n/th.po
Normal file
11173
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
11150
i18n/tr.po
Normal file
11150
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
11222
i18n/uk.po
Normal file
11222
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
11277
i18n/vi.po
Normal file
11277
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
10889
i18n/zh_CN.po
Normal file
10889
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
10871
i18n/zh_TW.po
Normal file
10871
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
23
models/__init__.py
Normal file
23
models/__init__.py
Normal 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
20
models/barcode.py
Normal 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',
|
||||
})
|
16
models/ir_actions_report.py
Normal file
16
models/ir_actions_report.py
Normal 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
1102
models/product.py
Normal file
File diff suppressed because it is too large
Load Diff
146
models/product_strategy.py
Normal file
146
models/product_strategy.py
Normal 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
208
models/res_company.py
Normal 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
|
134
models/res_config_settings.py
Normal file
134
models/res_config_settings.py
Normal 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
21
models/res_partner.py
Normal 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
474
models/stock_location.py
Normal 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
292
models/stock_lot.py
Normal 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
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
953
models/stock_move_line.py
Normal 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
591
models/stock_orderpoint.py
Normal 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)])
|
214
models/stock_package_level.py
Normal file
214
models/stock_package_level.py
Normal 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'},
|
||||
}
|
49
models/stock_package_type.py
Normal file
49
models/stock_package_type.py
Normal 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
1695
models/stock_picking.py
Normal file
File diff suppressed because it is too large
Load Diff
1482
models/stock_quant.py
Normal file
1482
models/stock_quant.py
Normal file
File diff suppressed because it is too large
Load Diff
603
models/stock_rule.py
Normal file
603
models/stock_rule.py
Normal 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
219
models/stock_scrap.py
Normal 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'
|
||||
}
|
68
models/stock_storage_category.py
Normal file
68
models/stock_storage_category.py
Normal 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
1085
models/stock_warehouse.py
Normal file
File diff suppressed because it is too large
Load Diff
5
populate/__init__.py
Normal file
5
populate/__init__.py
Normal 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
Loading…
x
Reference in New Issue
Block a user