mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-18 03:52:01 +02:00
19.0 vanilla
This commit is contained in:
parent
accf5918df
commit
6e65e8c877
688 changed files with 225434 additions and 199401 deletions
|
|
@ -10,39 +10,16 @@ pip install odoo-bringout-oca-ocb-mrp
|
|||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- product
|
||||
- stock
|
||||
- resource
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Manufacturing
|
||||
- **Version**: 2.0
|
||||
- **Category**: Manufacturing/Manufacturing
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `mrp`.
|
||||
- Repository: https://github.com/OCA/OCB
|
||||
- Branch: 19.0
|
||||
- Path: addons/mrp
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
This package preserves the original LGPL-3 license.
|
||||
|
|
|
|||
|
|
@ -5,35 +5,25 @@ from . import models
|
|||
from . import wizard
|
||||
from . import report
|
||||
from . import controller
|
||||
from . import populate
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def _pre_init_mrp(cr):
|
||||
def _pre_init_mrp(env):
|
||||
""" Allow installing MRP in databases with large stock.move table (>1M records)
|
||||
- Creating the computed+stored field stock_move.is_done and
|
||||
stock_move.unit_factor is terribly slow with the ORM and leads to "Out of
|
||||
Memory" crashes
|
||||
- Creating the computed stored fields `stock_move` `unit_factor` and `manual_consumption`
|
||||
is terribly slow with the ORM and leads to "Out of Memory" crashes.
|
||||
"""
|
||||
cr.execute("""ALTER TABLE "stock_move" ADD COLUMN "is_done" bool;""")
|
||||
cr.execute("""UPDATE stock_move
|
||||
SET is_done=COALESCE(state in ('done', 'cancel'), FALSE);""")
|
||||
cr.execute("""ALTER TABLE "stock_move" ADD COLUMN "unit_factor" double precision;""")
|
||||
cr.execute("""UPDATE stock_move
|
||||
SET unit_factor=1;""")
|
||||
env.cr.execute("""ALTER TABLE "stock_move" ADD COLUMN "unit_factor" double precision NOT NULL DEFAULT 1;""")
|
||||
env.cr.execute("""ALTER TABLE "stock_move" ADD COLUMN "manual_consumption" boolean NOT NULL DEFAULT FALSE;""")
|
||||
|
||||
def _create_warehouse_data(cr, registry):
|
||||
def _create_warehouse_data(env):
|
||||
""" This hook is used to add a default manufacture_pull_id, manufacture
|
||||
picking_type on every warehouse. It is necessary if the mrp module is
|
||||
installed after some warehouses were already created.
|
||||
"""
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
warehouse_ids = env['stock.warehouse'].search([('manufacture_pull_id', '=', False)])
|
||||
warehouse_ids.write({'manufacture_to_resupply': True})
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
def uninstall_hook(env):
|
||||
warehouses = env["stock.warehouse"].search([])
|
||||
pbm_routes = warehouses.mapped("pbm_route_id")
|
||||
warehouses.write({"pbm_route_id": False})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
'name': 'Manufacturing',
|
||||
'version': '2.0',
|
||||
'website': 'https://www.odoo.com/app/manufacturing',
|
||||
'category': 'Manufacturing/Manufacturing',
|
||||
'category': 'Supply Chain/Manufacturing',
|
||||
'sequence': 55,
|
||||
'summary': 'Manufacturing Orders & BOMs',
|
||||
'depends': ['product', 'stock', 'resource'],
|
||||
|
|
@ -16,14 +16,14 @@
|
|||
'data/digest_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/mrp_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'wizard/change_production_qty_views.xml',
|
||||
'wizard/mrp_workcenter_block_view.xml',
|
||||
'wizard/stock_warn_insufficient_qty_views.xml',
|
||||
'wizard/mrp_production_backorder.xml',
|
||||
'wizard/mrp_consumption_warning_views.xml',
|
||||
'wizard/mrp_immediate_production_views.xml',
|
||||
'wizard/stock_assign_serial_numbers.xml',
|
||||
'wizard/mrp_production_split.xml',
|
||||
'wizard/mrp_production_serial_numbers.xml',
|
||||
'views/mrp_views_menus.xml',
|
||||
'views/stock_move_views.xml',
|
||||
'views/mrp_workorder_views.xml',
|
||||
|
|
@ -31,19 +31,21 @@
|
|||
'views/mrp_bom_views.xml',
|
||||
'views/mrp_production_views.xml',
|
||||
'views/mrp_routing_views.xml',
|
||||
'views/product_document_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/stock_orderpoint_views.xml',
|
||||
'views/stock_warehouse_views.xml',
|
||||
'views/stock_picking_views.xml',
|
||||
'views/stock_rule_views.xml',
|
||||
'views/mrp_unbuild_views.xml',
|
||||
'views/ir_attachment_view.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/stock_scrap_views.xml',
|
||||
'wizard/stock_replenishment_info.xml', # needs views/mrp_workcenter_views.xml to load first
|
||||
'report/report_deliveryslip.xml',
|
||||
'report/mrp_report_views_main.xml',
|
||||
'report/mrp_report_bom_structure.xml',
|
||||
'report/mrp_report_mo_overview.xml',
|
||||
'report/mrp_production_templates.xml',
|
||||
'report/report_stock_forecasted.xml',
|
||||
'report/report_stock_reception.xml',
|
||||
'report/report_stock_rule.xml',
|
||||
'report/mrp_zebra_production_templates.xml',
|
||||
|
|
@ -63,9 +65,10 @@
|
|||
'web.assets_tests': [
|
||||
'mrp/static/tests/tours/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'web.assets_unit_tests': [
|
||||
'mrp/static/tests/**/*',
|
||||
],
|
||||
},
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools.translate import _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from odoo.addons.product.controllers.product_document import ProductDocumentController
|
||||
|
||||
|
||||
class MrpDocumentRoute(http.Controller):
|
||||
class MRPProductDocumentController(ProductDocumentController):
|
||||
|
||||
@http.route('/mrp/upload_attachment', type='http', methods=['POST'], auth="user")
|
||||
def upload_document(self, ufile, **kwargs):
|
||||
files = request.httprequest.files.getlist('ufile')
|
||||
result = {'success': _("All files uploaded")}
|
||||
for ufile in files:
|
||||
try:
|
||||
mimetype = ufile.content_type
|
||||
request.env['mrp.document'].create({
|
||||
'name': ufile.filename,
|
||||
'res_model': kwargs.get('res_model'),
|
||||
'res_id': int(kwargs.get('res_id')),
|
||||
'mimetype': mimetype,
|
||||
'datas': base64.encodebytes(ufile.read()),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("Fail to upload document %s" % ufile.filename)
|
||||
result = {'error': str(e)}
|
||||
|
||||
return json.dumps(result)
|
||||
def get_additional_create_params(self, **kwargs):
|
||||
super_values = super().get_additional_create_params(**kwargs)
|
||||
if kwargs.get('attached_on_bom'):
|
||||
return super_values | {'attached_on_mrp': 'bom'}
|
||||
return super_values
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<div>
|
||||
<p class="tip_title">Tip: Use tablets in the shop to control manufacturing</p>
|
||||
<p class="tip_content">With the Odoo work center control panel, your worker can start work orders in the shop and follow instructions of the worksheet. Quality tests are perfectly integrated into the process. Workers can trigger feedback loops, maintenance alerts, scrap products, etc.</p>
|
||||
<img src="https://download.odoocdn.com/digests/mrp/static/src/img/mrp-tablet.png" style="margin-top: 20px; max-width: 580px" width="100%" />
|
||||
<img src="https://download.odoocdn.com/digests/mrp/static/src/img/milk-mrp-tablet.png" width="540" class="illustration_border" />
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!-- new state-related subtypes-->
|
||||
<record id="mrp_mo_in_confirmed" model="mail.message.subtype">
|
||||
<field name="name">MO Confirmed</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="sequence" eval="101"/>
|
||||
<field name="description">MO Confirmed</field>
|
||||
</record>
|
||||
<record id="mrp_mo_in_progress" model="mail.message.subtype">
|
||||
<field name="name">MO Progress</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="sequence" eval="102"/>
|
||||
<field name="description">MO Progress</field>
|
||||
</record>
|
||||
<record id="mrp_mo_in_to_close" model="mail.message.subtype">
|
||||
<field name="name">MO To Close</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="sequence" eval="103"/>
|
||||
<field name="description">MO To Close</field>
|
||||
</record>
|
||||
<record id="mrp_mo_in_done" model="mail.message.subtype">
|
||||
<field name="name">MO Done</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="sequence" eval="104"/>
|
||||
<field name="description">MO Done</field>
|
||||
</record>
|
||||
<record id="mrp_mo_in_cancelled" model="mail.message.subtype">
|
||||
<field name="name">MO Cancelled</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="sequence" eval="105"/>
|
||||
<field name="description">MO Cancelled</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -14,13 +14,14 @@
|
|||
<t t-set="order" t-value="exception[0]"/>
|
||||
<t t-set="new_qty" t-value="exception[1][0]"/>
|
||||
<t t-set="old_qty" t-value="exception[1][1]"/>
|
||||
<t t-set="uom" t-value="move_raw_id.product_uom_id if 'product_uom_id' in move_raw_id else move_raw_id.product_uom"/>
|
||||
<a href="#" data-oe-model="mrp.production" t-att-data-oe-id="production_order.id"><t t-esc="production_order.name"/></a>:
|
||||
<t t-esc="new_qty"/> <t t-esc="move_raw_id.product_uom.name"/> of <t t-esc="move_raw_id.product_id.name"/>
|
||||
<t t-esc="new_qty"/> <t t-esc="uom.name"/> of <t t-esc="move_raw_id.product_id.name"/>
|
||||
<t t-if="cancel">
|
||||
cancelled
|
||||
</t>
|
||||
<t t-if="not cancel">
|
||||
ordered instead of <t t-esc="old_qty"/> <t t-esc="move_raw_id.product_uom.name"/>
|
||||
ordered instead of <t t-esc="old_qty"/> <t t-esc="uom.name"/>
|
||||
</t>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@
|
|||
<record id="route_warehouse0_manufacture" model='stock.route'>
|
||||
<field name="name">Manufacture</field>
|
||||
<field name="company_id"></field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="product_selectable" eval="False"/>
|
||||
<field name="warehouse_selectable" eval="True"/>
|
||||
<field name="warehouse_ids" eval="[(4, ref('stock.warehouse0'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Enable the manufacturing in warehouse0 -->
|
||||
|
|
|
|||
|
|
@ -3,28 +3,32 @@
|
|||
<data noupdate="1">
|
||||
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field eval="[(4, ref('group_mrp_user'))]" name="groups_id"/>
|
||||
<field eval="[(3, ref('group_mrp_manager')), (4, ref('group_mrp_user'))]" name="group_ids"/>
|
||||
</record>
|
||||
|
||||
<!-- Resource: res.company -->
|
||||
<record id="stock.res_company_1" model="res.company">
|
||||
<field eval="1.0" name="manufacturing_lead"/>
|
||||
<record id="base.default_user_group" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('mrp.group_mrp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Groups -->
|
||||
<record model="res.groups" id="base.group_user">
|
||||
<field name="implied_ids" eval="[(4, ref('mrp.group_mrp_routings'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Resource: mrp.workcenter -->
|
||||
|
||||
<record id="mrp_workcenter_3" model="mrp.workcenter">
|
||||
<field name="name">Assembly Line 1</field>
|
||||
<field name="name">Assembly 1</field>
|
||||
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_workcenter_1" model="mrp.workcenter">
|
||||
<field name="name">Drill Station 1</field>
|
||||
<field name="name">Drill 1</field>
|
||||
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_workcenter_2" model="mrp.workcenter">
|
||||
<field name="name">Assembly Line 2</field>
|
||||
<field name="name">Assembly 2</field>
|
||||
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -37,16 +41,14 @@
|
|||
<field name="product_tmpl_id" ref="product.product_product_3_product_template"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="produce_delay">10</field>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_manufacture"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="time_cycle_manual">60</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_manufacture_line_1" model="mrp.bom.line">
|
||||
|
|
@ -83,17 +85,17 @@
|
|||
|
||||
<record id="product_product_computer_desk" model="product.product">
|
||||
<field name="name">Table</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">290</field>
|
||||
<field name="list_price">520</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Solid wood table.</field>
|
||||
<field name="default_code">FURN_9666</field>
|
||||
<field name="tracking">serial</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/table.png"/>
|
||||
<field name="barcode">10133785575283</field>
|
||||
</record>
|
||||
<record id="stock_warehouse_orderpoint_table" model="stock.warehouse.orderpoint">
|
||||
<field name="product_max_qty">0.0</field>
|
||||
|
|
@ -107,54 +109,70 @@
|
|||
|
||||
<record id="product_product_computer_desk_head" model="product.product">
|
||||
<field name="name">Table Top</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">240</field>
|
||||
<field name="list_price">380</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Solid wood is a durable natural material.</field>
|
||||
<field name="default_code">FURN_8522</field>
|
||||
<field name="tracking">serial</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/table_top.png"/>
|
||||
<field name="route_ids" eval="[Command.link(ref('mrp.route_warehouse0_manufacture'))]"/>
|
||||
<field name="barcode">10133785575282</field>
|
||||
</record>
|
||||
<record id="product_product_computer_desk_leg" model="product.product">
|
||||
<field name="name">Table Leg</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">10</field>
|
||||
<field name="list_price">50</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">18″ x 2½″ Square Leg</field>
|
||||
<field name="default_code">FURN_2333</field>
|
||||
<field name="tracking">lot</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/table_leg.png"/>
|
||||
<field name="barcode">10133785575281</field>
|
||||
</record>
|
||||
<record id="product_supplierinfo_1" model="product.supplierinfo">
|
||||
<field name="product_tmpl_id" ref="product_product_computer_desk_leg_product_template"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="delay">3</field>
|
||||
<field name="min_qty">0</field>
|
||||
<field name="price">8</field>
|
||||
<field name="currency_id" ref="base.USD"/>
|
||||
</record>
|
||||
<record id="product_supplierinfo_2" model="product.supplierinfo">
|
||||
<field name="product_tmpl_id" ref="product_product_computer_desk_leg_product_template"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="delay">2</field>
|
||||
<field name="min_qty">0</field>
|
||||
<field name="price">10</field>
|
||||
<field name="currency_id" ref="base.USD"/>
|
||||
</record>
|
||||
<record id="product_product_computer_desk_bolt" model="product.product">
|
||||
<field name="name">Bolt</field>
|
||||
<field name="categ_id" ref="product.product_category_consumable"/>
|
||||
<field name="categ_id" ref="product.product_category_goods"/>
|
||||
<field name="standard_price">0.5</field>
|
||||
<field name="list_price">0.5</field>
|
||||
<field name="detailed_type">consu</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Stainless steel screw full (dia - 5mm, Length - 10mm)</field>
|
||||
<field name="default_code">CONS_89957</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_computer_desk_bolt.png"/>
|
||||
<field name="barcode">20133785543124</field>
|
||||
</record>
|
||||
<record id="product_product_computer_desk_screw" model="product.product">
|
||||
<field name="name">Screw</field>
|
||||
<field name="categ_id" ref="product.product_category_consumable"/>
|
||||
<field name="categ_id" ref="product.product_category_goods"/>
|
||||
<field name="standard_price">0.1</field>
|
||||
<field name="list_price">0.2</field>
|
||||
<field name="detailed_type">consu</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Stainless steel screw</field>
|
||||
<field name="default_code">CONS_25630</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_computer_desk_screw.png"/>
|
||||
|
|
@ -162,64 +180,60 @@
|
|||
|
||||
<record id="product_product_wood_ply" model="product.product">
|
||||
<field name="name">Ply Layer</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">10</field>
|
||||
<field name="list_price">10</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Layers that are stick together to assemble wood panels.</field>
|
||||
<field name="default_code">FURN_7111</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_wood_ply.png"/>
|
||||
</record>
|
||||
<record id="product_product_wood_wear" model="product.product">
|
||||
<field name="name">Wear Layer</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">10</field>
|
||||
<field name="list_price">10</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Top layer of a wood panel.</field>
|
||||
<field name="default_code">FURN_8111</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_wood_wear.png"/>
|
||||
</record>
|
||||
<record id="product_product_ply_veneer" model="product.product">
|
||||
<field name="name">Ply Veneer</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">10</field>
|
||||
<field name="list_price">10</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="default_code">FURN_9111</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_ply_veneer.png"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_wood_panel" model="product.product">
|
||||
<field name="name">Wood Panel</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">80</field>
|
||||
<field name="list_price">100</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="default_code">FURN_7023</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_wood_panel.png"/>
|
||||
<field name="route_ids" eval="[Command.link(ref('mrp.route_warehouse0_manufacture'))]"/>
|
||||
</record>
|
||||
<record id="product_product_plastic_laminate" model="product.product">
|
||||
<field name="name">Plastic Laminate</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">3000</field>
|
||||
<field name="list_price">1000</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="default_code">FURN_8621</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_plastic_laminate.png"/>
|
||||
</record>
|
||||
|
|
@ -233,16 +247,14 @@
|
|||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="consumption">flexible</field>
|
||||
<field name="days_to_prepare_mo">3</field>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_desk"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="time_cycle">120</field>
|
||||
<field name="time_cycle_manual">120</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="name">Assembly</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_desk_line_1" model="mrp.bom.line">
|
||||
|
|
@ -284,7 +296,7 @@
|
|||
<field name="product_id" ref="product_product_computer_desk"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="product_qty">1</field>
|
||||
<field name="date_planned_start" eval="(DateTime.today() + relativedelta(days=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="date_start" eval="(DateTime.today() + relativedelta(days=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="bom_id" ref="mrp_bom_desk"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -295,13 +307,10 @@
|
|||
</record>
|
||||
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_table_top"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="time_cycle_manual">60</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_line_wood_panel" model="mrp.bom.line">
|
||||
|
|
@ -326,35 +335,26 @@
|
|||
</record>
|
||||
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Long time assembly</field>
|
||||
<field name="time_cycle">180</field>
|
||||
<field name="time_cycle_manual">180</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Testing</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="time_cycle_manual">60</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_1"/>
|
||||
<field name="name">Packing</field>
|
||||
<field name="time_cycle">30</field>
|
||||
<field name="time_cycle_manual">30</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
<record id="mrp_bom_line_plastic_laminate" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_ply_veneer"/>
|
||||
|
|
@ -388,7 +388,7 @@
|
|||
<record id="mrp_production_4" model="mrp.production">
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="product_qty">2</field>
|
||||
<field name="product_qty">1</field>
|
||||
<field name="location_src_id" ref="stock.stock_location_stock"/>
|
||||
<field name="location_dest_id" ref="stock.stock_location_stock"/>
|
||||
<field name="bom_id" ref="mrp_bom_table_top"/>
|
||||
|
|
@ -397,13 +397,12 @@
|
|||
|
||||
<record id="product_product_table_kit" model="product.product">
|
||||
<field name="name">Table Kit</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="standard_price">600.0</field>
|
||||
<field name="list_price">147.0</field>
|
||||
<field name="detailed_type">consu</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Table kit</field>
|
||||
<field name="default_code">FURN_78236</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_table_kit.png"/>
|
||||
|
|
@ -439,14 +438,13 @@
|
|||
|
||||
<record id="product_product_drawer_drawer" model="product.product">
|
||||
<field name="name">Drawer Black</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="tracking">lot</field>
|
||||
<field name="standard_price">20.0</field>
|
||||
<field name="list_price">24.0</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description">Drawer on casters for great usability.</field>
|
||||
<field name="default_code">FURN_2100</field>
|
||||
<field name="barcode">601647855646</field>
|
||||
|
|
@ -455,44 +453,35 @@
|
|||
|
||||
<record id="product_product_drawer_case" model="product.product">
|
||||
<field name="name">Drawer Case Black</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="categ_id" ref="product.product_category_office"/>
|
||||
<field name="tracking">lot</field>
|
||||
<field name="standard_price">10</field>
|
||||
<field name="list_price">20</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="default_code">FURN_5623</field>
|
||||
<field name="barcode">601647855647</field>
|
||||
<field name="image_1920" type="base64" file="mrp/static/img/product_product_drawer_case_black.png"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_27" model="product.product">
|
||||
<field name="tracking">lot</field>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_27_0" model="stock.lot">
|
||||
<field name="name">0000000000030</field>
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
<record id="lot_product_27_1" model="stock.lot">
|
||||
<field name="name">0000000000031</field>
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_drawer_drawer_0" model="stock.lot">
|
||||
<field name="name">0000000010001</field>
|
||||
<field name="product_id" ref="product_product_drawer_drawer"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_drawer_case_0" model="stock.lot">
|
||||
<field name="name">0000000020045</field>
|
||||
<field name="product_id" ref="product_product_drawer_case"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
|
@ -580,35 +569,26 @@
|
|||
</record>
|
||||
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_drawer_rout"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Long time assembly</field>
|
||||
<field name="time_cycle">180</field>
|
||||
<field name="time_cycle_manual">180</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_drawer_rout"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Testing</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="time_cycle_manual">60</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_drawer_rout"/>
|
||||
<field name="active">False</field>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_1"/>
|
||||
<field name="name">Packing</field>
|
||||
<field name="time_cycle">30</field>
|
||||
<field name="time_cycle_manual">30</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet_type">pdf</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
<record id="mrp_bom_drawer_rout_line_1" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_drawer_drawer"/>
|
||||
|
|
@ -626,7 +606,7 @@
|
|||
</record>
|
||||
|
||||
<record id="product.product_product_27" model="product.product">
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="is_storable" eval="True"/>
|
||||
</record>
|
||||
<record id="mrp_production_drawer" model="mrp.production">
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
|
|
@ -638,7 +618,7 @@
|
|||
</record>
|
||||
|
||||
<!-- Run Scheduler -->
|
||||
<function model="procurement.group" name="run_scheduler"/>
|
||||
<function model="stock.rule" name="run_scheduler"/>
|
||||
|
||||
|
||||
<!-- OEE -->
|
||||
|
|
@ -685,18 +665,14 @@
|
|||
</record>
|
||||
|
||||
<function model="mrp.production" name="action_confirm" eval="[[
|
||||
ref('mrp.mrp_production_3'),
|
||||
ref('mrp.mrp_production_1'),
|
||||
ref('mrp.mrp_production_4'),
|
||||
ref('mrp.mrp_production_drawer'),
|
||||
]]"/>
|
||||
|
||||
<function model="mrp.production" name="button_plan">
|
||||
<value eval="[ref('mrp.mrp_production_3')]"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="write">
|
||||
<value eval="[ref('mrp.mrp_production_drawer')]"/>
|
||||
<value eval="{'qty_producing': 5, 'lot_producing_id': ref('mrp.lot_product_27_0')}"/>
|
||||
<value eval="{'qty_producing': 5, 'lot_producing_ids': [ref('mrp.lot_product_27_0')]}"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="action_assign">
|
||||
|
|
@ -705,7 +681,7 @@
|
|||
|
||||
<function model="stock.move" name="write">
|
||||
<value model="stock.move" eval="obj().env['stock.move'].search([('raw_material_production_id', '=', obj().env.ref('mrp.mrp_production_drawer').id)]).ids"/>
|
||||
<value eval="{'quantity_done': 5}"/>
|
||||
<value eval="{'quantity': 5, 'picked': True}"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="button_mark_done">
|
||||
|
|
@ -724,6 +700,81 @@
|
|||
<value eval="{'use_create_components_lots': True}"/>
|
||||
</function>
|
||||
|
||||
<record id="lot_product_product_computer_desk_head_1" model="stock.lot">
|
||||
<field name="name">TT001</field>
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
</record>
|
||||
<record id="lot_product_product_computer_desk_head_2" model="stock.lot">
|
||||
<field name="name">TT002</field>
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_computer_desk_head_3" model="stock.lot">
|
||||
<field name="name">TT003</field>
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_computer_desk_head_4" model="stock.lot">
|
||||
<field name="name">TT004</field>
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_computer_desk_head_5" model="stock.lot">
|
||||
<field name="name">TT005</field>
|
||||
<field name="product_id" ref="product_product_computer_desk_head"/>
|
||||
</record>
|
||||
|
||||
<record id="lot_product_product_computer_desk_1" model="stock.lot">
|
||||
<field name="name">T0001</field>
|
||||
<field name="product_id" ref="product_product_computer_desk"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="mrp_inventory_1" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk_head"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_head_1"/>
|
||||
</record>
|
||||
<record id="mrp_inventory_2" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk_head"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_head_2"/>
|
||||
</record>
|
||||
<record id="mrp_inventory_3" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk_head"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_head_3"/>
|
||||
</record>
|
||||
<record id="mrp_inventory_4" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk_head"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_head_4"/>
|
||||
</record>
|
||||
<record id="mrp_inventory_5" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk_head"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_head_5"/>
|
||||
</record>
|
||||
<record id="mrp_inventory_6" model="stock.quant">
|
||||
<field name="product_id" ref="mrp.product_product_computer_desk"/>
|
||||
<field name="inventory_quantity">1</field>
|
||||
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
<field name="lot_id" ref="lot_product_product_computer_desk_1"/>
|
||||
</record>
|
||||
|
||||
<function model="stock.quant" name="action_apply_inventory">
|
||||
<function eval="[[('id', 'in', [ref('mrp_inventory_1'),
|
||||
ref('mrp_inventory_2'),
|
||||
ref('mrp_inventory_3'),
|
||||
ref('mrp_inventory_4'),
|
||||
ref('mrp_inventory_5'),
|
||||
ref('mrp_inventory_6'),
|
||||
])]]" model="stock.quant" name="search"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,96 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * mrp
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux <mat@odoo.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 11.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-11-16 08:08+0000\n"
|
||||
"PO-Revision-Date: 2017-11-16 08:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux <mat@odoo.com>, 2017\n"
|
||||
"Language-Team: Spanish (Bolivia) (https://www.transifex.com/odoo/teams/41243/"
|
||||
"es_BO/)\n"
|
||||
"Language: es_BO\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: mrp
|
||||
#: model_terms:ir.ui.view,arch_db:mrp.mrp_workcenter_kanban
|
||||
msgid "<span>PLAN ORDERS</span>"
|
||||
msgstr "<span>PLANITICAR ÓRDENES</span>"
|
||||
|
||||
#. module: mrp
|
||||
#: model:ir.model.fields,field_description:mrp.field_change_production_qty__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_byproduct__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_line__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_document__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce_line__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_production__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing_workcenter__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_unbuild__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss_type__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder_line__create_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_stock_warn_insufficient_qty_unbuild__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creado en"
|
||||
|
||||
#. module: mrp
|
||||
#: model:ir.model.fields,field_description:mrp.field_change_production_qty__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_byproduct__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_line__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_document__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce_line__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_production__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing_workcenter__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_unbuild__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss_type__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder_line__write_uid
|
||||
#: model:ir.model.fields,field_description:mrp.field_stock_warn_insufficient_qty_unbuild__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualización de"
|
||||
|
||||
#. module: mrp
|
||||
#: model:ir.model.fields,field_description:mrp.field_change_production_qty__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_byproduct__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_bom_line__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_document__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_product_produce_line__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_production__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_routing_workcenter__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_unbuild__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workcenter_productivity_loss_type__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_mrp_workorder_line__write_date
|
||||
#: model:ir.model.fields,field_description:mrp.field_stock_warn_insufficient_qty_unbuild__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualización en"
|
||||
|
||||
#. module: mrp
|
||||
#: model:ir.ui.menu,name:mrp.menu_mrp_reporting
|
||||
msgid "Reporting"
|
||||
msgstr "Informe"
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_document
|
||||
from . import product_document
|
||||
from . import res_config_settings
|
||||
from . import mrp_bom
|
||||
from . import mrp_routing
|
||||
|
|
@ -13,10 +13,13 @@ from . import mrp_workorder
|
|||
from . import product
|
||||
from . import res_company
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_orderpoint
|
||||
from . import stock_picking
|
||||
from . import stock_lot
|
||||
from . import stock_reference
|
||||
from . import stock_rule
|
||||
from . import stock_scrap
|
||||
from . import stock_warehouse
|
||||
from . import stock_quant
|
||||
from . import stock_replenish_mixin
|
||||
|
|
|
|||
18
odoo-bringout-oca-ocb-mrp/mrp/models/ir_attachment.py
Normal file
18
odoo-bringout-oca-ocb-mrp/mrp/models/ir_attachment.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
def _post_add_create(self, **kwargs):
|
||||
super()._post_add_create(**kwargs)
|
||||
if self.res_model == "mrp.bom":
|
||||
bom = self.env['mrp.bom'].browse(self.res_id)
|
||||
self.res_model = bom.product_id._name if bom.product_id else bom.product_tmpl_id._name
|
||||
self.res_id = bom.product_id.id if bom.product_id else bom.product_tmpl_id.id
|
||||
self.env['product.document'].create({
|
||||
'ir_attachment_id': self.id,
|
||||
'attached_on_mrp': 'bom'
|
||||
})
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _, Command
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv.expression import AND, OR
|
||||
from odoo.tools import float_round
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools.misc import clean_context, OrderedSet
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ class MrpBom(models.Model):
|
|||
""" Defines bills of material for a product or a product template """
|
||||
_name = 'mrp.bom'
|
||||
_description = 'Bill of Material'
|
||||
_inherit = ['mail.thread']
|
||||
_inherit = ['mail.thread', 'product.catalog.mixin']
|
||||
_rec_name = 'product_tmpl_id'
|
||||
_rec_names_search = ['product_tmpl_id', 'code']
|
||||
_order = "sequence, id"
|
||||
|
|
@ -31,34 +31,38 @@ class MrpBom(models.Model):
|
|||
product_tmpl_id = fields.Many2one(
|
||||
'product.template', 'Product',
|
||||
check_company=True, index=True,
|
||||
domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True)
|
||||
domain="[('type', '=', 'consu')]", required=True)
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product Variant',
|
||||
check_company=True, index=True,
|
||||
domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', '=', 'consu')]",
|
||||
help="If a product variant is defined the BOM is available only for this product.")
|
||||
bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True)
|
||||
byproduct_ids = fields.One2many('mrp.bom.byproduct', 'bom_id', 'By-products', copy=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity', default=1.0,
|
||||
digits='Product Unit of Measure', required=True,
|
||||
digits='Product Unit', required=True,
|
||||
help="This should be the smallest quantity that this product can be produced in. If the BOM contains operations, make sure the work center capacity is accurate.")
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', 'Unit of Measure',
|
||||
'uom.uom', 'Unit',
|
||||
default=_get_default_product_uom_id, required=True,
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_tmpl_id.uom_id.category_id')
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control")
|
||||
sequence = fields.Integer('Sequence')
|
||||
operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True)
|
||||
operation_count = fields.Integer('Operations Count', compute='_compute_operation_count')
|
||||
show_copy_operations_button = fields.Boolean(
|
||||
compute="_compute_show_copy_operations_button",
|
||||
help="Technical field used to control the visibility of the 'Copy Existing Operations' button.")
|
||||
ready_to_produce = fields.Selection([
|
||||
('all_available', ' When all components are available'),
|
||||
('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness',
|
||||
default='all_available', required=True)
|
||||
picking_type_id = fields.Many2one(
|
||||
'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
|
||||
'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation')]",
|
||||
check_company=True,
|
||||
help=u"When a procurement has a ‘produce’ route with a operation type set, it will try to create "
|
||||
"a Manufacturing Order for that product using a BoM of the same operation type. That allows "
|
||||
"a Manufacturing Order for that product using a BoM of the same operation type.If not,"
|
||||
"the operation type is not taken into account in the BoM search. That allows "
|
||||
"to define stock rules which trigger different manufacturing orders with different BoMs.")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', index=True,
|
||||
|
|
@ -70,7 +74,7 @@ class MrpBom(models.Model):
|
|||
help="Defines if you can consume more or less components than the quantity defined on the BoM:\n"
|
||||
" * Allowed: allowed for all manufacturing users.\n"
|
||||
" * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
|
||||
" Note that in the case of component Manual Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n"
|
||||
" Note that in the case of component Highlight Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n"
|
||||
" * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
|
||||
default='warning',
|
||||
string='Flexible Consumption',
|
||||
|
|
@ -82,10 +86,20 @@ class MrpBom(models.Model):
|
|||
allow_operation_dependencies = fields.Boolean('Operation Dependencies',
|
||||
help="Create operation level dependencies that will influence both planning and the status of work orders upon MO confirmation. If this feature is ticked, and nothing is specified, Odoo will assume that all operations can be started simultaneously."
|
||||
)
|
||||
produce_delay = fields.Integer(
|
||||
'Manufacturing Lead Time', default=0,
|
||||
help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.")
|
||||
days_to_prepare_mo = fields.Integer(
|
||||
string="Days to prepare Manufacturing Order", default=0,
|
||||
help="Create and confirm Manufacturing Orders this many days in advance, to have enough time to replenish components or manufacture semi-finished products.")
|
||||
show_set_bom_button = fields.Boolean(compute="_compute_show_set_bom_button")
|
||||
batch_size = fields.Float('Batch Size', default=1.0, digits='Product Unit', help="All automatically generated manufacturing orders for this product will be of this size.")
|
||||
enable_batch_size = fields.Boolean(default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
|
||||
]
|
||||
_qty_positive = models.Constraint(
|
||||
'check (product_qty > 0)',
|
||||
'The quantity to produce must be positive!',
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'product_tmpl_id.attribute_line_ids.value_ids',
|
||||
|
|
@ -94,14 +108,26 @@ class MrpBom(models.Model):
|
|||
)
|
||||
def _compute_possible_product_template_attribute_value_ids(self):
|
||||
for bom in self:
|
||||
bom.possible_product_template_attribute_value_ids = bom.product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes().product_template_value_ids._only_active()
|
||||
bom.possible_product_template_attribute_value_ids = bom.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids._only_active()
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
warning = (
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids or
|
||||
self.operation_ids.bom_product_template_attribute_value_ids or
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids
|
||||
) and {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("Changing the product or variant will permanently reset all previously encoded variant-related data."),
|
||||
}
|
||||
}
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids = False
|
||||
self.operation_ids.bom_product_template_attribute_value_ids = False
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids = False
|
||||
if warning:
|
||||
return warning
|
||||
|
||||
@api.constrains('active', 'product_id', 'product_tmpl_id', 'bom_line_ids')
|
||||
def _check_bom_cycle(self):
|
||||
|
|
@ -118,8 +144,9 @@ class MrpBom(models.Model):
|
|||
for component in components:
|
||||
if component in finished_products:
|
||||
names = finished_products.mapped('display_name')
|
||||
raise ValidationError(_("The current configuration is incorrect because it would create a cycle "
|
||||
"between these products: %s.") % ', '.join(names))
|
||||
raise ValidationError(_(
|
||||
"The current configuration is incorrect because it would create a cycle between these products: %s.",
|
||||
', '.join(names)))
|
||||
if component not in subcomponents_dict:
|
||||
products_to_find |= component
|
||||
|
||||
|
|
@ -134,11 +161,11 @@ class MrpBom(models.Model):
|
|||
_check_cycle(subcomponents, finished_products | component)
|
||||
|
||||
boms_to_check = self
|
||||
domain = []
|
||||
for product in self.bom_line_ids.product_id:
|
||||
domain = OR([domain, self._bom_find_domain(product)])
|
||||
if domain:
|
||||
boms_to_check |= self.env['mrp.bom'].search(domain)
|
||||
if self.bom_line_ids.product_id:
|
||||
boms_to_check |= self.search(Domain.OR(
|
||||
self._bom_find_domain(product)
|
||||
for product in self.bom_line_ids.product_id
|
||||
))
|
||||
|
||||
for bom in boms_to_check:
|
||||
if not bom.active:
|
||||
|
|
@ -154,12 +181,6 @@ class MrpBom(models.Model):
|
|||
else:
|
||||
_check_cycle(bom.bom_line_ids.product_id, finished_products)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'sequence' in vals and self and self[-1].id == self._prefetch_ids[-1]:
|
||||
self.browse(self._prefetch_ids)._check_bom_cycle()
|
||||
return res
|
||||
|
||||
@api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids', 'byproduct_ids', 'operation_ids')
|
||||
def _check_bom_lines(self):
|
||||
for bom in self:
|
||||
|
|
@ -180,15 +201,17 @@ class MrpBom(models.Model):
|
|||
else:
|
||||
same_product = bom.product_tmpl_id == byproduct.product_id.product_tmpl_id
|
||||
if same_product:
|
||||
raise ValidationError(_("By-product %s should not be the same as BoM product.") % bom.display_name)
|
||||
raise ValidationError(_("By-product %s should not be the same as BoM product.", bom.display_name))
|
||||
if byproduct.cost_share < 0:
|
||||
raise ValidationError(_("By-products cost shares must be positive."))
|
||||
if sum(bom.byproduct_ids.mapped('cost_share')) > 100:
|
||||
raise ValidationError(_("The total cost share for a BoM's by-products cannot exceed 100."))
|
||||
for product in bom.product_tmpl_id.product_variant_ids:
|
||||
total_variant_cost_share = sum(bom.byproduct_ids.filtered(lambda bp: not bp._skip_byproduct_line(product) and not bp.product_uom_id.is_zero(bp.product_qty)).mapped('cost_share'))
|
||||
if float_compare(total_variant_cost_share, 100, precision_digits=2) > 0:
|
||||
raise ValidationError(_("The total cost share for a BoM's by-products cannot exceed 100."))
|
||||
|
||||
@api.onchange('bom_line_ids', 'product_qty', 'product_id', 'product_tmpl_id')
|
||||
def onchange_bom_structure(self):
|
||||
if self.type == 'phantom' and self._origin and self.env['stock.move'].search([('bom_line_id', 'in', self._origin.bom_line_ids.ids)], limit=1):
|
||||
if self.type == 'phantom' and self._origin and self.env['stock.move'].search_count([('bom_line_id', 'in', self._origin.bom_line_ids.ids)], limit=1):
|
||||
return {
|
||||
'warning': {
|
||||
'title': _('Warning'),
|
||||
|
|
@ -198,20 +221,23 @@ class MrpBom(models.Model):
|
|||
}
|
||||
}
|
||||
|
||||
@api.onchange('product_uom_id')
|
||||
def onchange_product_uom_id(self):
|
||||
res = {}
|
||||
if not self.product_uom_id or not self.product_tmpl_id:
|
||||
return
|
||||
if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
|
||||
return res
|
||||
|
||||
@api.onchange('product_tmpl_id')
|
||||
def onchange_product_tmpl_id(self):
|
||||
if self.product_tmpl_id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
warning = (
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids or
|
||||
self.operation_ids.bom_product_template_attribute_value_ids or
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids
|
||||
) and {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("Changing the product or variant will permanently reset all previously encoded variant-related data."),
|
||||
}
|
||||
}
|
||||
default_uom_id = self.env.context.get('default_product_uom_id')
|
||||
# Avoids updating the BoM's UoM in case a specific UoM was passed through as a default value.
|
||||
if self.product_uom_id.id != default_uom_id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
if self.product_id.product_tmpl_id != self.product_tmpl_id:
|
||||
self.product_id = False
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids = False
|
||||
|
|
@ -223,67 +249,136 @@ class MrpBom(models.Model):
|
|||
domain.append(('id', '!=', self.id.origin))
|
||||
number_of_bom_of_this_product = self.env['mrp.bom'].search_count(domain)
|
||||
if number_of_bom_of_this_product: # add a reference to the bom if there is already a bom for this product
|
||||
self.code = _("%s (new) %s", self.product_tmpl_id.name, number_of_bom_of_this_product)
|
||||
else:
|
||||
self.code = False
|
||||
self.code = _("%(product_name)s (new) %(number_of_boms)s", product_name=self.product_tmpl_id.name, number_of_boms=number_of_bom_of_this_product)
|
||||
if warning:
|
||||
return warning
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
# Checks if the BoM was created from a Manufacturing Order (through Generate BoM action).
|
||||
parent_production_id = self.env.context.get('parent_production_id')
|
||||
if parent_production_id: # In this case, assign the newly created BoM to the MO.
|
||||
# Clean context to avoid parasitic default values.
|
||||
env = self.env(context=clean_context(self.env.context))
|
||||
production = env['mrp.production'].browse(parent_production_id)
|
||||
production._link_bom(res[0])
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
relevant_fields = ['bom_line_ids', 'byproduct_ids', 'product_tmpl_id', 'product_id', 'product_qty']
|
||||
if any(field_name in vals for field_name in relevant_fields):
|
||||
self._set_outdated_bom_in_productions()
|
||||
if 'sequence' in vals and self and self[-1].id == list(self._prefetch_ids)[-1]:
|
||||
self.browse(self._prefetch_ids)._check_bom_cycle()
|
||||
return res
|
||||
|
||||
def copy(self, default=None):
|
||||
res = super().copy(default)
|
||||
if self.operation_ids:
|
||||
operations_mapping = {}
|
||||
for original, copied in zip(self.operation_ids, res.operation_ids.sorted()):
|
||||
operations_mapping[original] = copied
|
||||
for bom_line in res.bom_line_ids:
|
||||
if bom_line.operation_id:
|
||||
bom_line.operation_id = operations_mapping[bom_line.operation_id]
|
||||
for byproduct in res.byproduct_ids:
|
||||
if byproduct.operation_id:
|
||||
byproduct.operation_id = operations_mapping[byproduct.operation_id]
|
||||
for operation in self.operation_ids:
|
||||
if operation.blocked_by_operation_ids:
|
||||
copied_operation = operations_mapping[operation]
|
||||
dependencies = []
|
||||
for dependency in operation.blocked_by_operation_ids:
|
||||
dependencies.append(Command.link(operations_mapping[dependency].id))
|
||||
copied_operation.blocked_by_operation_ids = dependencies
|
||||
|
||||
return res
|
||||
new_boms = super().copy(default)
|
||||
for old_bom, new_bom in zip(self, new_boms):
|
||||
if old_bom.operation_ids:
|
||||
operations_mapping = {}
|
||||
for original, copied in zip(old_bom.operation_ids, new_bom.operation_ids.sorted()):
|
||||
operations_mapping[original] = copied
|
||||
for bom_line in new_bom.bom_line_ids:
|
||||
if bom_line.operation_id:
|
||||
bom_line.operation_id = operations_mapping[bom_line.operation_id]
|
||||
for byproduct in new_bom.byproduct_ids:
|
||||
if byproduct.operation_id:
|
||||
byproduct.operation_id = operations_mapping[byproduct.operation_id]
|
||||
for operation in old_bom.operation_ids:
|
||||
if operation.blocked_by_operation_ids:
|
||||
copied_operation = operations_mapping[operation]
|
||||
dependencies = []
|
||||
for dependency in operation.blocked_by_operation_ids:
|
||||
dependencies.append(Command.link(operations_mapping[dependency].id))
|
||||
copied_operation.blocked_by_operation_ids = dependencies
|
||||
return new_boms
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
# prevent to use string as product_tmpl_id
|
||||
if isinstance(name, str):
|
||||
key = 'default_' + self._rec_name
|
||||
if key in self.env.context:
|
||||
result = super().name_create(self.env.context[key])
|
||||
self.browse(result[0]).code = name
|
||||
return result
|
||||
raise UserError(_("You cannot create a new Bill of Material from here."))
|
||||
return super(MrpBom, self).name_create(name)
|
||||
|
||||
def toggle_active(self):
|
||||
self.with_context({'active_test': False}).operation_ids.toggle_active()
|
||||
return super().toggle_active()
|
||||
def action_archive(self):
|
||||
self.with_context(active_test=False).operation_ids.action_archive()
|
||||
return super().action_archive()
|
||||
|
||||
def name_get(self):
|
||||
return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self]
|
||||
def action_unarchive(self):
|
||||
self.with_context(active_test=False).operation_ids.action_unarchive()
|
||||
return super().action_unarchive()
|
||||
|
||||
@api.depends('code')
|
||||
def _compute_display_name(self):
|
||||
for bom in self:
|
||||
display_name = f"{bom.code + ': ' if bom.code else ''}{bom.product_tmpl_id.display_name}"
|
||||
if self.env.context.get('display_bom_uom_qty') and (bom.product_qty > 1 or bom.product_uom_id != bom.product_tmpl_id.uom_id):
|
||||
display_name += f" ({bom.product_qty} {bom.product_uom_id.name})"
|
||||
bom.display_name = _('%(display_name)s', display_name=display_name)
|
||||
|
||||
@api.depends('operation_ids')
|
||||
def _compute_operation_count(self):
|
||||
for bom in self:
|
||||
bom.operation_count = len(bom.operation_ids)
|
||||
|
||||
def _compute_show_copy_operations_button(self):
|
||||
exist_operation = bool(self.env['mrp.routing.workcenter'].search_count([], limit=1))
|
||||
self.show_copy_operations_button = exist_operation
|
||||
|
||||
def action_compute_bom_days(self):
|
||||
company_id = self.env.context.get('default_company_id', self.env.company.id)
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1)
|
||||
for bom in self:
|
||||
bom_data = self.env['report.mrp.report_bom_structure'].with_context(minimized=True)._get_bom_data(bom, warehouse, bom.product_id, ignore_stock=True)
|
||||
bom.days_to_prepare_mo = self.env['report.mrp.report_bom_structure']._get_max_component_delay(bom_data['components'])
|
||||
if bom_data.get('availability_state') == 'unavailable' and not bom_data.get('components_available', True):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Cannot compute days to prepare due to missing route info for at least 1 component or for the final product.'),
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.constrains('product_tmpl_id', 'product_id', 'type')
|
||||
def check_kit_has_not_orderpoint(self):
|
||||
product_ids = [pid for bom in self.filtered(lambda bom: bom.type == "phantom")
|
||||
for pid in (bom.product_id.ids or bom.product_tmpl_id.product_variant_ids.ids)]
|
||||
if self.env['stock.warehouse.orderpoint'].search([('product_id', 'in', product_ids)], count=True):
|
||||
if self.env['stock.warehouse.orderpoint'].search_count([('product_id', 'in', product_ids)], limit=1):
|
||||
raise ValidationError(_("You can not create a kit-type bill of materials for products that have at least one reordering rule."))
|
||||
|
||||
@api.constrains('enable_batch_size', 'batch_size')
|
||||
def _check_valid_batch_size(self):
|
||||
if any(bom.enable_batch_size and bom.product_uom_id.compare(bom.batch_size, 0.0) <= 0 for bom in self):
|
||||
raise ValidationError(self.env._("The batch size must be positive!"))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_running_mo(self):
|
||||
if self.env['mrp.production'].search([('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1):
|
||||
if self.env['mrp.production'].search_count([('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1):
|
||||
raise UserError(_('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'))
|
||||
|
||||
@api.model
|
||||
def _bom_find_domain(self, products, picking_type=None, company_id=False, bom_type=False):
|
||||
domain = ['&', '|', ('product_id', 'in', products.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', products.product_tmpl_id.ids), ('active', '=', True)]
|
||||
domain = (
|
||||
Domain('product_id', 'in', products.ids) | (
|
||||
Domain('product_id', '=', False) & Domain('product_tmpl_id', 'in', products.product_tmpl_id.ids)
|
||||
)
|
||||
) & Domain('active', '=', True)
|
||||
if company_id or self.env.context.get('company_id'):
|
||||
domain = AND([domain, ['|', ('company_id', '=', False), ('company_id', '=', company_id or self.env.context.get('company_id'))]])
|
||||
domain &= Domain('company_id', 'in', [False, company_id or self.env.context.get('company_id')])
|
||||
if picking_type:
|
||||
domain = AND([domain, ['|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False)]])
|
||||
domain &= Domain('picking_type_id', 'in', [picking_type.id, False])
|
||||
if bom_type:
|
||||
domain = AND([domain, [('type', '=', bom_type)]])
|
||||
domain &= Domain('type', '=', bom_type)
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
|
|
@ -318,29 +413,12 @@ class MrpBom(models.Model):
|
|||
|
||||
return bom_by_product
|
||||
|
||||
def explode(self, product, quantity, picking_type=False):
|
||||
def explode(self, product, quantity, picking_type=False, never_attribute_values=False):
|
||||
"""
|
||||
Explodes the BoM and creates two lists with all the information you need: bom_done and line_done
|
||||
Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
|
||||
and converted into its UoM
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
graph = defaultdict(list)
|
||||
V = set()
|
||||
|
||||
def check_cycle(v, visited, recStack, graph):
|
||||
visited[v] = True
|
||||
recStack[v] = True
|
||||
for neighbour in graph[v]:
|
||||
if visited[neighbour] == False:
|
||||
if check_cycle(neighbour, visited, recStack, graph) == True:
|
||||
return True
|
||||
elif recStack[neighbour] == True:
|
||||
return True
|
||||
recStack[v] = False
|
||||
return False
|
||||
|
||||
product_ids = set()
|
||||
product_boms = {}
|
||||
def update_product_boms():
|
||||
|
|
@ -351,15 +429,12 @@ class MrpBom(models.Model):
|
|||
for product in products:
|
||||
product_boms.setdefault(product, self.env['mrp.bom'])
|
||||
|
||||
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
|
||||
boms_done = [(self, self.env['mrp.bom.line']._prepare_bom_done_values(quantity, product, quantity, []))]
|
||||
lines_done = []
|
||||
V |= set([product.product_tmpl_id.id])
|
||||
|
||||
bom_lines = []
|
||||
for bom_line in self.bom_line_ids:
|
||||
product_id = bom_line.product_id
|
||||
V |= set([product_id.product_tmpl_id.id])
|
||||
graph[product.product_tmpl_id.id].append(product_id.product_tmpl_id.id)
|
||||
bom_lines.append((bom_line, product, quantity, False))
|
||||
product_ids.add(product_id.id)
|
||||
update_product_boms()
|
||||
|
|
@ -368,34 +443,36 @@ class MrpBom(models.Model):
|
|||
current_line, current_product, current_qty, parent_line = bom_lines[0]
|
||||
bom_lines = bom_lines[1:]
|
||||
|
||||
if current_line._skip_bom_line(current_product):
|
||||
if current_line._skip_bom_line(current_product, never_attribute_values):
|
||||
continue
|
||||
|
||||
line_quantity = current_qty * current_line.product_qty
|
||||
if not current_line.product_id in product_boms:
|
||||
if current_line.product_id not in product_boms:
|
||||
update_product_boms()
|
||||
product_ids.clear()
|
||||
bom = product_boms.get(current_line.product_id)
|
||||
if bom:
|
||||
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity / bom.product_qty, bom.product_uom_id)
|
||||
bom_lines += [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids]
|
||||
converted_line_quantity = current_line.product_uom_id._compute_quantity(
|
||||
line_quantity / bom.product_qty, bom.product_uom_id, round=False
|
||||
)
|
||||
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
|
||||
for bom_line in bom.bom_line_ids:
|
||||
graph[current_line.product_id.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph):
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
if not bom_line.product_id in product_boms:
|
||||
if bom_line.product_id not in product_boms:
|
||||
product_ids.add(bom_line.product_id.id)
|
||||
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
|
||||
boms_done.append((bom, current_line._prepare_bom_done_values(converted_line_quantity, current_product, quantity, boms_done)))
|
||||
else:
|
||||
# We round up here because the user expects that if he has to consume a little more, the whole UOM unit
|
||||
# should be consumed.
|
||||
rounding = current_line.product_uom_id.rounding
|
||||
line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP')
|
||||
lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line}))
|
||||
line_quantity = current_line.product_uom_id.round(line_quantity, rounding_method='UP')
|
||||
lines_done.append((current_line, current_line._prepare_line_done_values(line_quantity, current_product, quantity, parent_line, boms_done)))
|
||||
|
||||
lines_done = self._round_last_line_done(lines_done)
|
||||
return boms_done, lines_done
|
||||
|
||||
@api.model
|
||||
def _round_last_line_done(self, lines_done):
|
||||
return lines_done
|
||||
|
||||
@api.model
|
||||
def get_import_templates(self):
|
||||
return [{
|
||||
|
|
@ -403,6 +480,189 @@ class MrpBom(models.Model):
|
|||
'template': '/mrp/static/xls/mrp_bom.xls'
|
||||
}]
|
||||
|
||||
def _set_outdated_bom_in_productions(self):
|
||||
if not self:
|
||||
return
|
||||
# Searches for MOs using these BoMs to notify them that their BoM has been updated.
|
||||
list_of_domain_by_bom = []
|
||||
for bom in self:
|
||||
if bom.product_id:
|
||||
domain_by_products = Domain('product_id', '=', bom.product_id.id)
|
||||
else:
|
||||
domain_by_products = Domain('product_id', 'in', bom.product_tmpl_id.product_variant_ids.ids)
|
||||
domain_for_confirmed_mo = Domain('state', '=', 'confirmed') & domain_by_products
|
||||
# Avoid confirmed MOs if the BoM's product was changed.
|
||||
domain_by_states = Domain('state', '=', 'draft') | domain_for_confirmed_mo
|
||||
list_of_domain_by_bom.append(Domain('bom_id', '=', bom.id) & domain_by_states)
|
||||
productions = self.env['mrp.production'].search(Domain.OR(list_of_domain_by_bom))
|
||||
if productions:
|
||||
productions.is_outdated_bom = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_action_add_from_catalog_extra_context(self):
|
||||
return {
|
||||
**super()._get_action_add_from_catalog_extra_context(),
|
||||
'product_catalog_currency_id': self.env.company.currency_id.id,
|
||||
}
|
||||
|
||||
def _default_order_line_values(self, child_field=False):
|
||||
default_data = super()._default_order_line_values(child_field)
|
||||
new_default_data = self[child_field]._get_product_catalog_lines_data(default=True)
|
||||
|
||||
return {**default_data, **new_default_data}
|
||||
|
||||
def _get_product_catalog_order_data(self, products, **kwargs):
|
||||
product_catalog = super()._get_product_catalog_order_data(products, **kwargs)
|
||||
for product in products:
|
||||
product_catalog[product.id] |= self._get_product_price_and_data(product)
|
||||
return product_catalog
|
||||
|
||||
def _get_product_price_and_data(self, product):
|
||||
self.ensure_one()
|
||||
return {'price': product.standard_price}
|
||||
|
||||
def _get_product_catalog_record_lines(self, product_ids, *, child_field=False, **kwargs):
|
||||
if not child_field:
|
||||
return {}
|
||||
lines = self[child_field].filtered(lambda line: line.product_id.id in product_ids)
|
||||
return lines.grouped('product_id')
|
||||
|
||||
def _update_order_line_info(self, product_id, quantity, *, child_field=False, **kwargs):
|
||||
if not child_field:
|
||||
return 0
|
||||
entity = self[child_field].filtered(lambda line: line.product_id.id == product_id)
|
||||
if entity:
|
||||
if quantity != 0:
|
||||
entity.product_qty = quantity
|
||||
else:
|
||||
entity.unlink()
|
||||
elif quantity > 0:
|
||||
command = Command.create({
|
||||
'product_qty': quantity,
|
||||
'product_id': product_id,
|
||||
})
|
||||
self.write({child_field: [command]})
|
||||
|
||||
return self.env['product.product'].browse(product_id).standard_price
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# DOCUMENT
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_mail_thread_data_attachments(self):
|
||||
res = super()._get_mail_thread_data_attachments()
|
||||
return res | self._get_extra_attachments()
|
||||
|
||||
def _get_extra_attachments(self):
|
||||
is_byproduct = self.env.user.has_group('mrp.group_mrp_byproducts')
|
||||
product_ids, template_ids = OrderedSet(), OrderedSet()
|
||||
for bom in self:
|
||||
product_ids.add(bom.product_id.id)
|
||||
template_ids.add(bom.product_tmpl_id.id)
|
||||
if is_byproduct:
|
||||
product_ids.update(bom.byproduct_ids.product_id.ids)
|
||||
template_ids.update(bom.byproduct_ids.product_id.product_tmpl_id.ids)
|
||||
|
||||
domain = Domain('attached_on_mrp', '=', 'bom') & (
|
||||
(Domain('res_model', '=', 'product.product') & Domain('res_id', 'in', product_ids))
|
||||
| (Domain('res_model', '=', 'product.template') & Domain('res_id', 'in', template_ids))
|
||||
)
|
||||
attachements = self.env['product.document'].search(domain).ir_attachment_id
|
||||
return attachements
|
||||
|
||||
@api.model
|
||||
def _skip_for_no_variant(self, product, bom_attribule_values, never_attribute_values=False):
|
||||
""" Controls if a Component/Operation/Byproduct line should be skipped based on the 'no_variant' attributes
|
||||
Cases:
|
||||
- no_variant:
|
||||
1. attribute present on the line
|
||||
=> need to be at least one attribute value matching between the one passed as args and the ones one the line
|
||||
2. attribute not present on the line
|
||||
=> valid if the line has no attribute value selected for that attribute
|
||||
- always and dynamic: match_all_variant_values()
|
||||
"""
|
||||
no_variant_bom_attributes = bom_attribule_values.filtered(lambda av: av.attribute_id.create_variant == 'no_variant')
|
||||
|
||||
# Attributes create_variant 'always' and 'dynamic'
|
||||
other_attribute_valid = product._match_all_variant_values(bom_attribule_values - no_variant_bom_attributes)
|
||||
|
||||
# If there are no never attribute values on the line => 'always' and 'dynamic'
|
||||
if not no_variant_bom_attributes:
|
||||
return not other_attribute_valid
|
||||
|
||||
# Or if there are never attribute on the line values but no value is passed => impossible to match
|
||||
if not never_attribute_values:
|
||||
return True
|
||||
|
||||
bom_values_by_attribute = no_variant_bom_attributes.grouped('attribute_id')
|
||||
never_values_by_attribute = never_attribute_values.grouped('attribute_id')
|
||||
|
||||
# Or if there is no overlap between given line values attributes and the ones on on the bom
|
||||
if not any(never_att_id in no_variant_bom_attributes.attribute_id.ids for never_att_id in never_attribute_values.attribute_id.ids):
|
||||
return True
|
||||
|
||||
# Check that at least one variant attribute is correct
|
||||
for attribute, values in bom_values_by_attribute.items():
|
||||
if never_values_by_attribute.get(attribute) and any(val.id in never_values_by_attribute[attribute].ids for val in values):
|
||||
return not other_attribute_valid
|
||||
|
||||
# None were found, so we skip the line
|
||||
return True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# REPLENISHMENT WIZARD
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _compute_show_set_bom_button(self):
|
||||
self.show_set_bom_button = True
|
||||
orderpoint_id = self.env.context.get('orderpoint_id', self.env.context.get('default_orderpoint_id'))
|
||||
if orderpoint_id:
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
self.filtered(
|
||||
lambda s: s.id == orderpoint.bom_id.id
|
||||
).show_set_bom_button = False
|
||||
|
||||
def action_set_bom_on_orderpoint(self):
|
||||
self.ensure_one()
|
||||
orderpoint_id = self.env.context.get('orderpoint_id')
|
||||
if not orderpoint_id:
|
||||
return
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
if 'manufacture' not in orderpoint.route_id.rule_ids.mapped('action'):
|
||||
domain = Domain.AND([
|
||||
[('action', '=', 'manufacture')],
|
||||
Domain.OR([
|
||||
[('company_id', '=', orderpoint.company_id.id)],
|
||||
[('company_id', '=', False)],
|
||||
]),
|
||||
])
|
||||
orderpoint.route_id = self.env['stock.rule'].search(domain, limit=1).route_id.id
|
||||
orderpoint.bom_id = self
|
||||
bom_qty = self.product_uom_id._compute_quantity(self.product_qty, orderpoint.product_id.uom_id)
|
||||
if orderpoint.qty_to_order < bom_qty:
|
||||
orderpoint.qty_to_order = bom_qty
|
||||
return orderpoint.action_stock_replenishment_info()
|
||||
|
||||
def action_open_operation_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mrp.routing.workcenter',
|
||||
'context': {
|
||||
'default_bom_id': self.id,
|
||||
'search_default_bom_id': self.id,
|
||||
'bom_id_invisible': True,
|
||||
},
|
||||
}
|
||||
|
||||
def action_copy_existing_operations(self):
|
||||
self.ensure_one()
|
||||
return self.env['mrp.routing.workcenter'].with_context(bom_id=self.id).copy_existing_operations()
|
||||
|
||||
|
||||
class MrpBomLine(models.Model):
|
||||
_name = 'mrp.bom.line'
|
||||
|
|
@ -414,19 +674,16 @@ class MrpBomLine(models.Model):
|
|||
def _get_default_product_uom_id(self):
|
||||
return self.env['uom.uom'].search([], limit=1, order='id').id
|
||||
|
||||
product_id = fields.Many2one('product.product', 'Component', required=True, check_company=True)
|
||||
product_id = fields.Many2one('product.product', 'Component', required=True, check_company=True, index=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id', store=True, index=True)
|
||||
company_id = fields.Many2one(
|
||||
related='bom_id.company_id', store=True, index=True, readonly=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity', default=1.0,
|
||||
digits='Product Unit of Measure', required=True)
|
||||
digits='Product Unit', required=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', 'Product Unit of Measure',
|
||||
default=_get_default_product_uom_id,
|
||||
required=True,
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
'uom.uom', 'Unit',
|
||||
default=_get_default_product_uom_id, required=True)
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=1,
|
||||
help="Gives the sequence order when displaying.")
|
||||
|
|
@ -451,29 +708,11 @@ class MrpBomLine(models.Model):
|
|||
compute='_compute_child_line_ids')
|
||||
attachments_count = fields.Integer('Attachments Count', compute='_compute_attachments_count')
|
||||
tracking = fields.Selection(related='product_id.tracking')
|
||||
manual_consumption = fields.Boolean(
|
||||
'Manual Consumption', default=False, compute='_compute_manual_consumption',
|
||||
readonly=False, store=True, copy=True,
|
||||
help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n"
|
||||
"If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.")
|
||||
manual_consumption_readonly = fields.Boolean(
|
||||
'Manual Consumption Readonly', compute='_compute_manual_consumption_readonly')
|
||||
|
||||
_sql_constraints = [
|
||||
('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n'
|
||||
'Lines with 0 quantities can be used as optional lines. \n'
|
||||
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
|
||||
]
|
||||
|
||||
@api.depends('product_id', 'tracking', 'operation_id')
|
||||
def _compute_manual_consumption(self):
|
||||
for line in self:
|
||||
line.manual_consumption = (line.tracking != 'none' or line.operation_id)
|
||||
|
||||
@api.depends('tracking', 'operation_id')
|
||||
def _compute_manual_consumption_readonly(self):
|
||||
for line in self:
|
||||
line.manual_consumption_readonly = (line.tracking != 'none' or line.operation_id)
|
||||
_bom_qty_zero = models.Constraint(
|
||||
'CHECK (product_qty>=0)',
|
||||
'All product quantities must be greater or equal to 0.\nLines with 0 quantities can be used as optional lines. \nYou should install the mrp_byproduct module if you want to manage extra products on BoMs!',
|
||||
)
|
||||
|
||||
@api.depends('product_id', 'bom_id')
|
||||
def _compute_child_bom_id(self):
|
||||
|
|
@ -488,10 +727,11 @@ class MrpBomLine(models.Model):
|
|||
@api.depends('product_id')
|
||||
def _compute_attachments_count(self):
|
||||
for line in self:
|
||||
nbr_attach = self.env['mrp.document'].search_count([
|
||||
nbr_attach = self.env['product.document'].search_count([
|
||||
'&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'),
|
||||
'|',
|
||||
'&', ('res_model', '=', 'product.product'), ('res_id', '=', line.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_id.product_tmpl_id.id)])
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_tmpl_id.id)])
|
||||
line.attachments_count = nbr_attach
|
||||
|
||||
@api.depends('child_bom_id')
|
||||
|
|
@ -500,16 +740,6 @@ class MrpBomLine(models.Model):
|
|||
for line in self:
|
||||
line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False
|
||||
|
||||
@api.onchange('product_uom_id')
|
||||
def onchange_product_uom_id(self):
|
||||
res = {}
|
||||
if not self.product_uom_id or not self.product_id:
|
||||
return res
|
||||
if self.product_uom_id.category_id != self.product_id.uom_id.category_id:
|
||||
self.product_uom_id = self.product_id.uom_id.id
|
||||
res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
|
||||
return res
|
||||
|
||||
@api.onchange('product_id')
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
|
|
@ -522,40 +752,91 @@ class MrpBomLine(models.Model):
|
|||
values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id
|
||||
return super(MrpBomLine, self).create(vals_list)
|
||||
|
||||
def _skip_bom_line(self, product):
|
||||
""" Control if a BoM line should be produced, can be inherited to add
|
||||
custom control.
|
||||
def _skip_bom_line(self, product, never_attribute_values=False):
|
||||
""" Control if a BoM line should be produced, can be inherited to add custom control.
|
||||
cases:
|
||||
- no_variant:
|
||||
1. attribute present on the line
|
||||
=> need to be at least one attribute value matching between the one passed as args and the ones one the line
|
||||
2. attribute not present on the line
|
||||
=> valid if the line has no attribute value selected for that attribute
|
||||
- always and dynamic: match_all_variant_values()
|
||||
"""
|
||||
self.ensure_one()
|
||||
if product._name == 'product.template':
|
||||
if not product or product._name == 'product.template':
|
||||
return False
|
||||
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
|
||||
|
||||
return self.env['mrp.bom']._skip_for_no_variant(product, self.bom_product_template_attribute_value_ids, never_attribute_values)
|
||||
|
||||
def action_see_attachments(self):
|
||||
domain = [
|
||||
'&', ('attached_on_mrp', '=', 'bom'),
|
||||
'|',
|
||||
'&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id)]
|
||||
attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp')
|
||||
attachments = self.env['product.document'].search(domain)
|
||||
nbr_product_attach = len(attachments.filtered(lambda a: a.res_model == 'product.product'))
|
||||
nbr_template_attach = len(attachments.filtered(lambda a: a.res_model == 'product.template'))
|
||||
context = {'default_res_model': 'product.product',
|
||||
'default_res_id': self.product_id.id,
|
||||
'default_company_id': self.company_id.id,
|
||||
'attached_on_bom': True,
|
||||
'search_default_context_variant': not (nbr_product_attach == 0 and nbr_template_attach > 0) if self.env.user.has_group('product.group_product_variant') else False
|
||||
}
|
||||
|
||||
return {
|
||||
'name': _('Attachments'),
|
||||
'domain': domain,
|
||||
'res_model': 'mrp.document',
|
||||
'res_model': 'product.document',
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_id': attachment_view.id,
|
||||
'views': [(attachment_view.id, 'kanban'), (False, 'form')],
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'target': 'current',
|
||||
'help': _('''<p class="o_view_nocontent_smiling_face">
|
||||
Upload files to your product
|
||||
</p><p>
|
||||
Use this feature to store any files, like drawings or specifications.
|
||||
</p>'''),
|
||||
'limit': 80,
|
||||
'context': "{'default_res_model': '%s','default_res_id': %d, 'default_company_id': %s}" % ('product.product', self.product_id.id, self.company_id.id)
|
||||
'context': context,
|
||||
'search_view_id': self.env.ref('product.product_document_search').ids
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class MrpByProduct(models.Model):
|
||||
def action_add_from_catalog(self):
|
||||
bom = self.env['mrp.bom'].browse(self.env.context.get('order_id'))
|
||||
return bom.with_context(child_field='bom_line_ids').action_add_from_catalog()
|
||||
|
||||
def _get_product_catalog_lines_data(self, default=False, **kwargs):
|
||||
if self and not default:
|
||||
self.product_id.ensure_one()
|
||||
return {
|
||||
**self[0].bom_id._get_product_price_and_data(self[0].product_id),
|
||||
'quantity': sum(
|
||||
self.mapped(
|
||||
lambda line: line.product_uom_id._compute_quantity(
|
||||
qty=line.product_qty,
|
||||
to_unit=line.product_uom_id,
|
||||
)
|
||||
)
|
||||
),
|
||||
'readOnly': len(self) > 1,
|
||||
'uomDisplayName': len(self) == 1 and self.product_uom_id.display_name or self.product_id.uom_id.display_name,
|
||||
}
|
||||
return {
|
||||
'quantity': 0,
|
||||
}
|
||||
|
||||
def _prepare_bom_done_values(self, quantity, product, original_quantity, boms_done):
|
||||
return {'qty': quantity, 'product': product, 'original_qty': original_quantity, 'parent_line': self}
|
||||
|
||||
def _prepare_line_done_values(self, quantity, product, original_quantity, parent_line, boms_done):
|
||||
return {'qty': quantity, 'product': product, 'original_qty': original_quantity, 'parent_line': parent_line}
|
||||
|
||||
|
||||
class MrpBomByproduct(models.Model):
|
||||
_name = 'mrp.bom.byproduct'
|
||||
_description = 'Byproduct'
|
||||
_rec_name = "product_id"
|
||||
|
|
@ -566,11 +847,9 @@ class MrpByProduct(models.Model):
|
|||
company_id = fields.Many2one(related='bom_id.company_id', store=True, index=True, readonly=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity',
|
||||
default=1.0, digits='Product Unit of Measure', required=True)
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True,
|
||||
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
default=1.0, digits='Product Unit', required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit', required=True,
|
||||
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True)
|
||||
bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade', index=True)
|
||||
allowed_operation_ids = fields.One2many('mrp.routing.workcenter', related='bom_id.operation_ids')
|
||||
operation_id = fields.Many2one(
|
||||
|
|
@ -593,11 +872,40 @@ class MrpByProduct(models.Model):
|
|||
for record in self:
|
||||
record.product_uom_id = record.product_id.uom_id.id
|
||||
|
||||
def _skip_byproduct_line(self, product):
|
||||
def _skip_byproduct_line(self, product, never_attribute_values=False):
|
||||
""" Control if a byproduct line should be produced, can be inherited to add
|
||||
custom control.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if product._name == 'product.template':
|
||||
if not product or product._name == 'product.template':
|
||||
return False
|
||||
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
|
||||
|
||||
return self.env['mrp.bom']._skip_for_no_variant(product, self.bom_product_template_attribute_value_ids, never_attribute_values)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def action_add_from_catalog(self):
|
||||
bom = self.env['mrp.bom'].browse(self.env.context.get('order_id'))
|
||||
return bom.with_context(child_field='byproduct_ids').action_add_from_catalog()
|
||||
|
||||
def _get_product_catalog_lines_data(self, default=False, **kwargs):
|
||||
if self and not default:
|
||||
self.product_id.ensure_one()
|
||||
return {
|
||||
**self[0].bom_id._get_product_price_and_data(self[0].product_id),
|
||||
'quantity': sum(
|
||||
self.mapped(
|
||||
lambda line: line.product_uom_id._compute_quantity(
|
||||
qty=line.product_qty,
|
||||
to_unit=line.product_uom_id,
|
||||
)
|
||||
)
|
||||
),
|
||||
'readOnly': len(self) > 1,
|
||||
'uomDisplayName': len(self) == 1 and self.product_uom_id.display_name or self.product_id.uom_id.display_name,
|
||||
}
|
||||
return {
|
||||
'quantity': 0,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MrpDocument(models.Model):
|
||||
""" Extension of ir.attachment only used in MRP to handle archivage
|
||||
and basic versioning.
|
||||
"""
|
||||
_name = 'mrp.document'
|
||||
_description = "Production Document"
|
||||
_inherits = {
|
||||
'ir.attachment': 'ir_attachment_id',
|
||||
}
|
||||
_order = "priority desc, id desc"
|
||||
|
||||
def copy(self, default=None):
|
||||
ir_default = default
|
||||
if ir_default:
|
||||
ir_fields = list(self.env['ir.attachment']._fields)
|
||||
ir_default = {field : default[field] for field in default.keys() if field in ir_fields}
|
||||
new_attach = self.ir_attachment_id.with_context(no_document=True).copy(ir_default)
|
||||
return super().copy(dict(default, ir_attachment_id=new_attach.id))
|
||||
|
||||
ir_attachment_id = fields.Many2one('ir.attachment', string='Related attachment', required=True, ondelete='cascade')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
priority = fields.Selection([
|
||||
('0', 'Normal'),
|
||||
('1', 'Low'),
|
||||
('2', 'High'),
|
||||
('3', 'Very High')], string="Priority") # used to order
|
||||
|
||||
def unlink(self):
|
||||
self.mapped('ir_attachment_id').unlink()
|
||||
return super(MrpDocument, self).unlink()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _, tools
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_round, float_is_zero
|
||||
|
||||
|
||||
class MrpRoutingWorkcenter(models.Model):
|
||||
_name = 'mrp.routing.workcenter'
|
||||
_description = 'Work Center Usage'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
_order = 'bom_id, sequence, id'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char('Operation', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', required=True, check_company=True)
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', required=True, check_company=True, tracking=True, index=True)
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=100,
|
||||
help="Gives the sequence order when displaying a list of routing Work Centers.")
|
||||
|
|
@ -21,25 +24,18 @@ class MrpRoutingWorkcenter(models.Model):
|
|||
'mrp.bom', 'Bill of Material',
|
||||
index=True, ondelete='cascade', required=True, check_company=True)
|
||||
company_id = fields.Many2one('res.company', 'Company', related='bom_id.company_id')
|
||||
worksheet_type = fields.Selection([
|
||||
('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')],
|
||||
string="Worksheet", default="text"
|
||||
)
|
||||
note = fields.Html('Description')
|
||||
worksheet = fields.Binary('PDF')
|
||||
worksheet_google_slide = fields.Char('Google Slide', help="Paste the url of your Google Slide. Make sure the access to the document is public.")
|
||||
time_mode = fields.Selection([
|
||||
('auto', 'Compute based on tracked time'),
|
||||
('manual', 'Set duration manually')], string='Duration Computation',
|
||||
default='manual')
|
||||
('manual', 'Fixed'),
|
||||
('auto', 'Computed')], string='Duration Computation',
|
||||
default='manual', tracking=True)
|
||||
time_mode_batch = fields.Integer('Based on', default=10)
|
||||
time_computed_on = fields.Char('Computed on last', compute='_compute_time_computed_on')
|
||||
time_cycle_manual = fields.Float(
|
||||
'Manual Duration', default=60,
|
||||
'Manual Duration', default=60, tracking=True,
|
||||
help="Time in minutes:"
|
||||
"- In manual mode, time used"
|
||||
"- In automatic mode, supposed first time when there aren't any work orders yet")
|
||||
time_cycle = fields.Float('Duration', compute="_compute_time_cycle")
|
||||
"- In fixed mode, time used"
|
||||
"- In computed mode, supposed first time when there aren't any work orders yet")
|
||||
time_cycle = fields.Float('Cycles', compute="_compute_time_cycle")
|
||||
workorder_count = fields.Integer("# Work Orders", compute="_compute_workorder_count")
|
||||
workorder_ids = fields.One2many('mrp.workorder', 'operation_id', string="Work Orders")
|
||||
possible_bom_product_template_attribute_value_ids = fields.Many2many(related='bom_id.possible_product_template_attribute_value_ids')
|
||||
|
|
@ -58,20 +54,33 @@ class MrpRoutingWorkcenter(models.Model):
|
|||
string="Blocks", help="Operations that cannot start before this operation is completed.",
|
||||
domain="[('allow_operation_dependencies', '=', True), ('id', '!=', id), ('bom_id', '=', bom_id)]",
|
||||
copy=False)
|
||||
cycle_number = fields.Integer("Repetitions", compute="_compute_time_cycle")
|
||||
time_total = fields.Float('Total Duration', compute="_compute_time_cycle")
|
||||
show_time_total = fields.Boolean('Show Total Duration?', compute="_compute_time_cycle")
|
||||
cost_mode = fields.Selection([('actual', 'Actual time'), ('estimated', 'Theorical time')],
|
||||
string='Cost based on', default='actual', tracking=True,
|
||||
help="Determines the way Odoo calculates the cost of the operation:\n"
|
||||
"- Based on Actual time: the cost will be calculated based on tracked time and real employee costs.\n"
|
||||
"- Based on Estimated time: the cost will be calculated based on estimated time and costs.")
|
||||
cost = fields.Float('Cost', compute="_compute_cost")
|
||||
|
||||
@api.depends('time_mode', 'time_mode_batch')
|
||||
def _compute_time_computed_on(self):
|
||||
for operation in self:
|
||||
operation.time_computed_on = _('%i work orders') % operation.time_mode_batch if operation.time_mode != 'manual' else False
|
||||
operation.time_computed_on = _('%i work orders', operation.time_mode_batch) if operation.time_mode != 'manual' else False
|
||||
|
||||
@api.depends('time_cycle_manual', 'time_mode', 'workorder_ids')
|
||||
@api.depends('time_cycle_manual', 'time_mode', 'workorder_ids',
|
||||
'bom_id.product_id', 'bom_id.product_qty',
|
||||
'workcenter_id.time_start', 'workcenter_id.time_stop', 'workcenter_id.capacity_ids'
|
||||
)
|
||||
@api.depends_context('product', 'quantity', 'unit', 'workcenter')
|
||||
def _compute_time_cycle(self):
|
||||
manual_ops = self.filtered(lambda operation: operation.time_mode == 'manual')
|
||||
for operation in manual_ops:
|
||||
operation.time_cycle = operation.time_cycle_manual
|
||||
for operation in self - manual_ops:
|
||||
data = self.env['mrp.workorder'].search([
|
||||
('operation_id', '=', operation.id),
|
||||
('operation_id', 'in', operation.ids),
|
||||
('qty_produced', '>', 0),
|
||||
('state', '=', 'done')],
|
||||
limit=operation.time_mode_batch,
|
||||
|
|
@ -85,33 +94,74 @@ class MrpRoutingWorkcenter(models.Model):
|
|||
cycle_number = 0 # Never 0 unless infinite item['workcenter_id'].capacity
|
||||
for item in data:
|
||||
total_duration += item['duration']
|
||||
capacity = item['workcenter_id']._get_capacity(item.product_id)
|
||||
qty_produced = item.product_uom_id._compute_quantity(item['qty_produced'], item.product_id.uom_id)
|
||||
cycle_number += tools.float_round((qty_produced / capacity or 1.0), precision_digits=0, rounding_method='UP')
|
||||
(capacity, _setup, _cleanup) = item['workcenter_id']._get_capacity(item.product_id, item.product_uom_id, operation.bom_id.product_qty or 1)
|
||||
cycle_number += float_round((item['qty_produced'] / capacity), precision_digits=0, rounding_method='UP')
|
||||
if cycle_number:
|
||||
operation.time_cycle = total_duration / cycle_number
|
||||
else:
|
||||
operation.time_cycle = operation.time_cycle_manual
|
||||
|
||||
for operation in self:
|
||||
workcenter = self.env.context.get('workcenter', operation.workcenter_id)
|
||||
product = self.env.context.get('product', operation.bom_id.product_id or operation.bom_id.product_tmpl_id.product_variant_ids)
|
||||
if len(product) > 1:
|
||||
operation.cycle_number = 1
|
||||
operation.time_total = workcenter.time_start + workcenter.time_stop + operation.time_cycle_manual
|
||||
operation.show_time_total = False
|
||||
continue
|
||||
quantity = self.env.context.get('quantity', operation.bom_id.product_qty or 1)
|
||||
unit = self.env.context.get('unit', operation.bom_id.product_uom_id)
|
||||
(capacity, setup, cleanup) = workcenter._get_capacity(product, unit, operation.bom_id.product_qty or 1)
|
||||
operation.cycle_number = float_round(quantity / capacity, precision_digits=0, rounding_method="UP")
|
||||
operation.time_total = setup + cleanup + operation.cycle_number * operation.time_cycle * 100.0 / (workcenter.time_efficiency or 100.0)
|
||||
operation.show_time_total = operation.cycle_number > 1 or not float_is_zero(setup + cleanup, precision_digits=0)
|
||||
|
||||
def _compute_workorder_count(self):
|
||||
data = self.env['mrp.workorder']._read_group([
|
||||
('operation_id', 'in', self.ids),
|
||||
('state', '=', 'done')], ['operation_id'], ['operation_id'])
|
||||
count_data = dict((item['operation_id'][0], item['operation_id_count']) for item in data)
|
||||
('state', '=', 'done')], ['operation_id'], ['__count'])
|
||||
count_data = {operation.id: count for operation, count in data}
|
||||
for operation in self:
|
||||
operation.workorder_count = count_data.get(operation.id, 0)
|
||||
|
||||
@api.depends('time_total', 'workcenter_id')
|
||||
@api.depends_context('product', 'quantity', 'unit', 'workcenter')
|
||||
def _compute_cost(self):
|
||||
for operation in self:
|
||||
operation.cost = (operation.time_total / 60.0) * operation.workcenter_id.costs_hour
|
||||
|
||||
@api.constrains('blocked_by_operation_ids')
|
||||
def _check_no_cyclic_dependencies(self):
|
||||
if not self._check_m2m_recursion('blocked_by_operation_ids'):
|
||||
if self._has_cycle('blocked_by_operation_ids'):
|
||||
raise ValidationError(_("You cannot create cyclic dependency."))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
res.bom_id._set_outdated_bom_in_productions()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
self.bom_id._set_outdated_bom_in_productions()
|
||||
if 'bom_id' in vals:
|
||||
for op in self:
|
||||
op.bom_id.bom_line_ids.filtered(lambda line: line.operation_id == op).operation_id = False
|
||||
op.bom_id.byproduct_ids.filtered(lambda byproduct: byproduct.operation_id == op).operation_id = False
|
||||
op.bom_id.operation_ids.filtered(lambda operation: operation.blocked_by_operation_ids == op).blocked_by_operation_ids = False
|
||||
return super().write(vals)
|
||||
|
||||
def action_archive(self):
|
||||
res = super().action_archive()
|
||||
bom_lines = self.env['mrp.bom.line'].search([('operation_id', 'in', self.ids)])
|
||||
bom_lines.write({'operation_id': False})
|
||||
byproduct_lines = self.env['mrp.bom.byproduct'].search([('operation_id', 'in', self.ids)])
|
||||
byproduct_lines.write({'operation_id': False})
|
||||
self.bom_id._set_outdated_bom_in_productions()
|
||||
return res
|
||||
|
||||
def action_unarchive(self):
|
||||
res = super().action_unarchive()
|
||||
self.bom_id._set_outdated_bom_in_productions()
|
||||
return res
|
||||
|
||||
def copy_to_bom(self):
|
||||
|
|
@ -132,15 +182,15 @@ class MrpRoutingWorkcenter(models.Model):
|
|||
'type': 'ir.actions.act_window',
|
||||
'name': _('Select Operations to Copy'),
|
||||
'res_model': 'mrp.routing.workcenter',
|
||||
'view_mode': 'tree,form',
|
||||
'view_mode': 'list,form',
|
||||
'domain': ['|', ('bom_id', '=', False), ('bom_id.active', '=', True)],
|
||||
'context' : {
|
||||
'bom_id': self.env.context["bom_id"],
|
||||
'tree_view_ref': 'mrp.mrp_routing_workcenter_copy_to_bom_tree_view',
|
||||
'list_view_ref': 'mrp.mrp_routing_workcenter_copy_to_bom_tree_view',
|
||||
}
|
||||
}
|
||||
|
||||
def _skip_operation_line(self, product):
|
||||
def _skip_operation_line(self, product, never_attribute_values=False):
|
||||
""" Control if a operation should be processed, can be inherited to add
|
||||
custom control.
|
||||
"""
|
||||
|
|
@ -148,20 +198,14 @@ class MrpRoutingWorkcenter(models.Model):
|
|||
# skip operation line if archived
|
||||
if not self.active:
|
||||
return True
|
||||
if product._name == 'product.template':
|
||||
if not product or product._name == 'product.template':
|
||||
return False
|
||||
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
|
||||
|
||||
def _get_comparison_values(self):
|
||||
if not self:
|
||||
return False
|
||||
self.ensure_one()
|
||||
return tuple(self[key] for key in ('name', 'company_id', 'workcenter_id', 'time_mode', 'time_cycle_manual', 'bom_product_template_attribute_value_ids'))
|
||||
return self.env['mrp.bom']._skip_for_no_variant(product, self.bom_product_template_attribute_value_ids, never_attribute_values)
|
||||
|
||||
def write(self, values):
|
||||
if 'bom_id' in values:
|
||||
for op in self:
|
||||
op.bom_id.bom_line_ids.filtered(lambda line: line.operation_id == op).operation_id = False
|
||||
op.bom_id.byproduct_ids.filtered(lambda byproduct: byproduct.operation_id == op).operation_id = False
|
||||
op.bom_id.operation_ids.filtered(lambda operation: operation.blocked_by_operation_ids == op).blocked_by_operation_ids = False
|
||||
return super().write(values)
|
||||
def action_open_operation_form(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mrp.routing.workcenter',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,38 @@
|
|||
# -*- 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, ValidationError
|
||||
from odoo.tools import float_compare, float_round
|
||||
from odoo.osv import expression
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_round
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
|
||||
class MrpUnbuild(models.Model):
|
||||
_name = "mrp.unbuild"
|
||||
_name = 'mrp.unbuild'
|
||||
_description = "Unbuild Order"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'id desc'
|
||||
|
||||
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
|
||||
name = fields.Char('Reference', copy=False, readonly=True, default=lambda s: s.env._('New'))
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product', check_company=True,
|
||||
domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
required=True, states={'done': [('readonly', True)]})
|
||||
domain="[('type', '=', 'consu')]",
|
||||
compute='_compute_product_id', store=True, precompute=True, readonly=False,
|
||||
required=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda s: s.env.company,
|
||||
required=True, index=True, states={'done': [('readonly', True)]})
|
||||
required=True, index=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity', default=1.0,
|
||||
required=True, states={'done': [('readonly', True)]})
|
||||
digits='Product Unit',
|
||||
compute='_compute_product_qty', store=True, precompute=True, readonly=False,
|
||||
required=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', 'Unit of Measure',
|
||||
'uom.uom', 'Unit',
|
||||
compute='_compute_product_uom_id', store=True, readonly=False, precompute=True,
|
||||
required=True, states={'done': [('readonly', True)]})
|
||||
required=True)
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Bill of Material',
|
||||
domain="""[
|
||||
|
|
@ -43,30 +45,31 @@ class MrpUnbuild(models.Model):
|
|||
'|',
|
||||
('company_id', '=', company_id),
|
||||
('company_id', '=', False)
|
||||
]
|
||||
""",
|
||||
states={'done': [('readonly', True)]}, check_company=True)
|
||||
]""",
|
||||
compute='_compute_bom_id', store=True,
|
||||
check_company=True)
|
||||
mo_id = fields.Many2one(
|
||||
'mrp.production', 'Manufacturing Order',
|
||||
domain="[('state', '=', 'done'), ('company_id', '=', company_id), ('product_id', '=?', product_id), ('bom_id', '=?', bom_id)]",
|
||||
states={'done': [('readonly', True)]}, check_company=True)
|
||||
domain="[('state', '=', 'done'), ('product_id', '=?', product_id), ('bom_id', '=?', bom_id)]",
|
||||
check_company=True, index='btree_not_null')
|
||||
mo_bom_id = fields.Many2one('mrp.bom', 'Bill of Material used on the Production Order', related='mo_id.bom_id')
|
||||
lot_producing_ids = fields.Many2many('stock.lot', string='Lot/Serial Numbers', related='mo_id.lot_producing_ids')
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot', 'Lot/Serial Number',
|
||||
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True)
|
||||
has_tracking=fields.Selection(related='product_id.tracking', readonly=True)
|
||||
domain="[('product_id', '=', product_id),('id', 'in', lot_producing_ids)]", check_company=True)
|
||||
has_tracking = fields.Selection(related='product_id.tracking', readonly=True)
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Source Location',
|
||||
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
domain="[('usage','=','internal')]",
|
||||
check_company=True,
|
||||
compute='_compute_location_id', store=True, readonly=False, precompute=True,
|
||||
required=True, states={'done': [('readonly', True)]}, help="Location where the product you want to unbuild is.")
|
||||
required=True, help="Location where the product you want to unbuild is.")
|
||||
location_dest_id = fields.Many2one(
|
||||
'stock.location', 'Destination Location',
|
||||
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
domain="[('usage','=','internal')]",
|
||||
check_company=True,
|
||||
compute='_compute_location_id', store=True, readonly=False, precompute=True,
|
||||
required=True, states={'done': [('readonly', True)]}, help="Location where you want to send the components resulting from the unbuild order.")
|
||||
required=True, help="Location where you want to send the components resulting from the unbuild order.")
|
||||
consume_line_ids = fields.One2many(
|
||||
'stock.move', 'consume_unbuild_id', readonly=True,
|
||||
string='Consumed Disassembly Lines')
|
||||
|
|
@ -77,6 +80,11 @@ class MrpUnbuild(models.Model):
|
|||
('draft', 'Draft'),
|
||||
('done', 'Done')], string='Status', default='draft')
|
||||
|
||||
_qty_positive = models.Constraint(
|
||||
'check (product_qty > 0)',
|
||||
'The quantity to unbuild must be positive!',
|
||||
)
|
||||
|
||||
@api.depends('mo_id', 'product_id')
|
||||
def _compute_product_uom_id(self):
|
||||
for record in self:
|
||||
|
|
@ -95,30 +103,30 @@ class MrpUnbuild(models.Model):
|
|||
if order.location_dest_id.company_id != order.company_id:
|
||||
order.location_dest_id = warehouse.lot_stock_id
|
||||
|
||||
@api.onchange('mo_id')
|
||||
def _onchange_mo_id(self):
|
||||
if self.mo_id:
|
||||
self.product_id = self.mo_id.product_id.id
|
||||
self.bom_id = self.mo_id.bom_id
|
||||
self.product_uom_id = self.mo_id.product_uom_id
|
||||
self.lot_id = self.mo_id.lot_producing_id
|
||||
if self.has_tracking == 'serial':
|
||||
self.product_qty = 1
|
||||
@api.depends('mo_id', 'product_id', 'company_id')
|
||||
def _compute_bom_id(self):
|
||||
for order in self:
|
||||
if order.mo_id:
|
||||
order.bom_id = order.mo_id.bom_id
|
||||
else:
|
||||
self.product_qty = self.mo_id.qty_produced
|
||||
order.bom_id = self.env['mrp.bom']._bom_find(
|
||||
order.product_id, company_id=order.company_id.id
|
||||
)[order.product_id]
|
||||
|
||||
@api.depends('mo_id')
|
||||
def _compute_product_id(self):
|
||||
for order in self:
|
||||
if order.mo_id and order.mo_id.product_id:
|
||||
order.product_id = order.mo_id.product_id
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.bom_id = self.env['mrp.bom']._bom_find(self.product_id, company_id=self.company_id.id)[self.product_id]
|
||||
self.product_uom_id = self.mo_id.product_id == self.product_id and self.mo_id.product_uom_id.id or self.product_id.uom_id.id
|
||||
|
||||
@api.constrains('product_qty')
|
||||
def _check_qty(self):
|
||||
for unbuild in self:
|
||||
if unbuild.product_qty <= 0:
|
||||
raise ValidationError(_('Unbuild Order product quantity has to be strictly positive.'))
|
||||
@api.depends('mo_id')
|
||||
def _compute_product_qty(self):
|
||||
for order in self:
|
||||
if order.mo_id:
|
||||
if order.has_tracking == 'serial':
|
||||
order.product_qty = 1
|
||||
else:
|
||||
order.product_qty = order.mo_id.qty_produced
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
@ -136,7 +144,7 @@ class MrpUnbuild(models.Model):
|
|||
return {
|
||||
'move_id': finished_move.id,
|
||||
'lot_id': self.lot_id.id,
|
||||
'qty_done': finished_move.product_uom_qty,
|
||||
'quantity': finished_move.product_uom_qty - finished_move.quantity,
|
||||
'product_id': finished_move.product_id.id,
|
||||
'product_uom_id': finished_move.product_uom.id,
|
||||
'location_id': finished_move.location_id.id,
|
||||
|
|
@ -147,7 +155,7 @@ class MrpUnbuild(models.Model):
|
|||
return {
|
||||
'move_id': move.id,
|
||||
'lot_id': origin_move_line.lot_id.id,
|
||||
'qty_done': taken_quantity,
|
||||
'quantity': taken_quantity,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_id': origin_move_line.product_uom_id.id,
|
||||
'location_id': move.location_id.id,
|
||||
|
|
@ -157,70 +165,86 @@ class MrpUnbuild(models.Model):
|
|||
def action_unbuild(self):
|
||||
self.ensure_one()
|
||||
self._check_company()
|
||||
# remove the default_* keys that were only needed in the unbuild wizard
|
||||
self = self.with_env(self.env(context=clean_context(self.env.context))) # noqa: PLW0642
|
||||
if self.product_id.tracking != 'none' and not self.lot_id.id:
|
||||
raise UserError(_('You should provide a lot number for the final product.'))
|
||||
|
||||
if self.mo_id:
|
||||
if self.mo_id.state != 'done':
|
||||
raise UserError(_('You cannot unbuild a undone manufacturing order.'))
|
||||
if self.mo_id and self.mo_id.state != 'done':
|
||||
raise UserError(_('You cannot unbuild a undone manufacturing order.'))
|
||||
|
||||
consume_moves = self._generate_consume_moves()
|
||||
consume_moves._action_confirm()
|
||||
produce_moves = self._generate_produce_moves()
|
||||
produce_moves.with_context(default_lot_id=False)._action_confirm()
|
||||
produce_moves._action_confirm()
|
||||
produce_moves.quantity = 0
|
||||
|
||||
# Collect component lots already restored by previous unbuilds on the same MO
|
||||
previously_unbuilt_lots = (self.mo_id.unbuild_ids - self).produce_line_ids.filtered(lambda ml: ml.product_id != self.product_id and ml.product_id.tracking == 'serial').lot_ids
|
||||
|
||||
finished_moves = consume_moves.filtered(lambda m: m.product_id == self.product_id)
|
||||
consume_moves -= finished_moves
|
||||
error_message = _(
|
||||
"Please specify a manufacturing order.\n"
|
||||
"It will allow us to retrieve the lots/serial numbers of the correct components and/or byproducts."
|
||||
)
|
||||
|
||||
if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves):
|
||||
raise UserError(_('Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components.'))
|
||||
raise UserError(error_message)
|
||||
|
||||
if any(consume_move.has_tracking != 'none' and not self.mo_id for consume_move in consume_moves):
|
||||
raise UserError(_('Some of your byproducts are tracked, you have to specify a manufacturing order in order to retrieve the correct byproducts.'))
|
||||
raise UserError(error_message)
|
||||
|
||||
for finished_move in finished_moves:
|
||||
if finished_move.has_tracking != 'none':
|
||||
if float_compare(finished_move.product_uom_qty, finished_move.quantity, precision_rounding=finished_move.product_uom.rounding) > 0:
|
||||
finished_move_line_vals = self._prepare_finished_move_line_vals(finished_move)
|
||||
self.env["stock.move.line"].create(finished_move_line_vals)
|
||||
else:
|
||||
finished_move.quantity_done = self.product_qty
|
||||
self.env['stock.move.line'].create(finished_move_line_vals)
|
||||
|
||||
# TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took
|
||||
qty_already_used = defaultdict(float)
|
||||
for move in produce_moves | consume_moves:
|
||||
if move.has_tracking != 'none':
|
||||
original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids
|
||||
original_move = original_move.filtered(lambda m: m.product_id == move.product_id)
|
||||
needed_quantity = move.product_uom_qty
|
||||
moves_lines = original_move.mapped('move_line_ids')
|
||||
if move in produce_moves and self.lot_id:
|
||||
moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.produce_line_ids.lot_id) # FIXME sle: double check with arm
|
||||
for move_line in moves_lines:
|
||||
# Iterate over all move_lines until we unbuilded the correct quantity.
|
||||
taken_quantity = min(needed_quantity, move_line.qty_done - qty_already_used[move_line])
|
||||
if taken_quantity:
|
||||
move_line_vals = self._prepare_move_line_vals(move, move_line, taken_quantity)
|
||||
self.env["stock.move.line"].create(move_line_vals)
|
||||
needed_quantity -= taken_quantity
|
||||
qty_already_used[move_line] += taken_quantity
|
||||
else:
|
||||
move.quantity_done = float_round(move.product_uom_qty, precision_rounding=move.product_uom.rounding)
|
||||
if float_compare(move.product_uom_qty, move.quantity, precision_rounding=move.product_uom.rounding) < 1:
|
||||
continue
|
||||
original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids
|
||||
original_move = original_move.filtered(lambda m: m.product_id == move.product_id)
|
||||
if not original_move:
|
||||
move.quantity = move.product_uom.round(move.product_uom_qty)
|
||||
continue
|
||||
needed_quantity = move.product_uom_qty
|
||||
moves_lines = original_move.mapped('move_line_ids')
|
||||
if move in produce_moves and self.lot_id:
|
||||
moves_lines = moves_lines.filtered(
|
||||
lambda ml: self.lot_id in ml.produce_line_ids.lot_id and ml.lot_id not in previously_unbuilt_lots
|
||||
)
|
||||
for move_line in moves_lines:
|
||||
# Iterate over all move_lines until we unbuilded the correct quantity.
|
||||
taken_quantity = min(needed_quantity, move_line.quantity - qty_already_used[move_line])
|
||||
taken_quantity = move.product_uom.round(taken_quantity)
|
||||
if taken_quantity:
|
||||
move_line_vals = self._prepare_move_line_vals(move, move_line, taken_quantity)
|
||||
if move_line.owner_id:
|
||||
move_line_vals['owner_id'] = move_line.owner_id.id
|
||||
unbuild_move_line = self.env["stock.move.line"].create(move_line_vals)
|
||||
needed_quantity -= taken_quantity
|
||||
qty_already_used[move_line] += taken_quantity
|
||||
unbuild_move_line._apply_putaway_strategy()
|
||||
|
||||
(finished_moves | consume_moves | produce_moves).picked = True
|
||||
finished_moves._action_done()
|
||||
consume_moves._action_done()
|
||||
produce_moves._action_done()
|
||||
produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0)
|
||||
produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.quantity > 0)
|
||||
consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]})
|
||||
if self.mo_id:
|
||||
unbuild_msg = _(
|
||||
"%(qty)s %(measure)s unbuilt in %(order)s",
|
||||
unbuild_msg = _("%(qty)s %(measure)s unbuilt in %(order)s",
|
||||
qty=self.product_qty,
|
||||
measure=self.product_uom_id.name,
|
||||
order=self._get_html_link(),
|
||||
)
|
||||
self.mo_id.message_post(
|
||||
body=unbuild_msg,
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return self.write({'state': 'done'})
|
||||
|
||||
def _generate_consume_moves(self):
|
||||
|
|
@ -228,8 +252,7 @@ class MrpUnbuild(models.Model):
|
|||
for unbuild in self:
|
||||
if unbuild.mo_id:
|
||||
finished_moves = unbuild.mo_id.move_finished_ids.filtered(lambda move: move.state == 'done')
|
||||
moved_qty = unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.qty_produced, unbuild.product_uom_id)
|
||||
factor = unbuild.product_qty / moved_qty if moved_qty else 0
|
||||
factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.qty_produced, unbuild.product_uom_id)
|
||||
for finished_move in finished_moves:
|
||||
moves += unbuild._generate_move_from_existing_move(finished_move, factor, unbuild.location_id, finished_move.location_id)
|
||||
else:
|
||||
|
|
@ -259,10 +282,9 @@ class MrpUnbuild(models.Model):
|
|||
|
||||
def _generate_move_from_existing_move(self, move, factor, location_id, location_dest_id):
|
||||
return self.env['stock.move'].create({
|
||||
'name': self.name,
|
||||
'date': self.create_date,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_qty': move.quantity_done * factor,
|
||||
'product_uom_qty': move.quantity * factor,
|
||||
'product_uom': move.product_uom.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
'location_dest_id': location_dest_id.id,
|
||||
|
|
@ -279,7 +301,6 @@ class MrpUnbuild(models.Model):
|
|||
location_dest_id = bom_line_id and self.location_dest_id or product_prod_location
|
||||
warehouse = location_dest_id.warehouse_id
|
||||
return self.env['stock.move'].create({
|
||||
'name': self.name,
|
||||
'date': self.create_date,
|
||||
'bom_line_id': bom_line_id,
|
||||
'byproduct_id': byproduct_id,
|
||||
|
|
@ -296,14 +317,14 @@ class MrpUnbuild(models.Model):
|
|||
|
||||
def action_validate(self):
|
||||
self.ensure_one()
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit')
|
||||
available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
|
||||
unbuild_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_id.uom_id)
|
||||
if float_compare(available_qty, unbuild_qty, precision_digits=precision) >= 0:
|
||||
return self.action_unbuild()
|
||||
else:
|
||||
return {
|
||||
'name': self.product_id.display_name + _(': Insufficient Quantity To Unbuild'),
|
||||
'name': _('%(product)s: Insufficient Quantity To Unbuild', product=self.product_id.display_name),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.warn.insufficient.qty.unbuild',
|
||||
'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id,
|
||||
|
|
@ -313,7 +334,7 @@ class MrpUnbuild(models.Model):
|
|||
'default_location_id': self.location_id.id,
|
||||
'default_unbuild_id': self.id,
|
||||
'default_quantity': unbuild_qty,
|
||||
'default_product_uom_name': self.product_id.uom_name
|
||||
'default_product_uom_name': self.product_id.uom_name,
|
||||
},
|
||||
'target': 'new'
|
||||
'target': 'new',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
|
||||
from babel.dates import format_date
|
||||
from collections import defaultdict
|
||||
from dateutil import relativedelta
|
||||
from datetime import timedelta, datetime
|
||||
from functools import partial
|
||||
|
|
@ -9,15 +12,17 @@ from random import randint
|
|||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.resource.models.resource import make_aware, Intervals
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.tools.intervals import Intervals
|
||||
from odoo.tools.date_utils import start_of, end_of, localized, to_timezone
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
||||
from odoo.tools.misc import get_lang
|
||||
|
||||
|
||||
class MrpWorkcenter(models.Model):
|
||||
_name = 'mrp.workcenter'
|
||||
_description = 'Work Center'
|
||||
_order = "sequence, id"
|
||||
_inherit = ['resource.mixin']
|
||||
_inherit = ['mail.thread', 'resource.mixin']
|
||||
_check_company_auto = True
|
||||
|
||||
# resource
|
||||
|
|
@ -28,23 +33,21 @@ class MrpWorkcenter(models.Model):
|
|||
code = fields.Char('Code', copy=False)
|
||||
note = fields.Html(
|
||||
'Description')
|
||||
default_capacity = fields.Float(
|
||||
'Capacity', default=1.0,
|
||||
help="Default number of pieces (in product UoM) that can be produced in parallel (at the same time) at this work center. For example: the capacity is 5 and you need to produce 10 units, then the operation time listed on the BOM will be multiplied by two. However, note that both time before and after production will only be counted once.")
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=1, required=True,
|
||||
help="Gives the sequence order when displaying a list of work centers.")
|
||||
color = fields.Integer('Color')
|
||||
currency_id = fields.Many2one('res.currency', 'Currency', related='company_id.currency_id', readonly=True, required=True)
|
||||
costs_hour = fields.Float(string='Cost per hour', help='Hourly processing cost.', default=0.0)
|
||||
costs_hour = fields.Float(string='Cost per hour', help='Hourly processing cost.', default=0.0, tracking=True)
|
||||
time_start = fields.Float('Setup Time')
|
||||
time_stop = fields.Float('Cleanup Time')
|
||||
routing_line_ids = fields.One2many('mrp.routing.workcenter', 'workcenter_id', "Routing Lines")
|
||||
has_routing_lines = fields.Boolean(compute='_compute_has_routing_lines', help='Technical field for workcenter views')
|
||||
order_ids = fields.One2many('mrp.workorder', 'workcenter_id', "Orders")
|
||||
workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count')
|
||||
workorder_ready_count = fields.Integer('# Read Work Orders', compute='_compute_workorder_count')
|
||||
workorder_ready_count = fields.Integer('# To Do Work Orders', compute='_compute_workorder_count')
|
||||
workorder_progress_count = fields.Integer('Total Running Orders', compute='_compute_workorder_count')
|
||||
workorder_pending_count = fields.Integer('Total Pending Orders', compute='_compute_workorder_count')
|
||||
workorder_blocked_count = fields.Integer('Total Pending Orders', compute='_compute_workorder_count')
|
||||
workorder_late_count = fields.Integer('Total Late Orders', compute='_compute_workorder_count')
|
||||
|
||||
time_ids = fields.One2many('mrp.workcenter.productivity', 'workcenter_id', 'Time Logs')
|
||||
|
|
@ -74,6 +77,15 @@ class MrpWorkcenter(models.Model):
|
|||
tag_ids = fields.Many2many('mrp.workcenter.tag')
|
||||
capacity_ids = fields.One2many('mrp.workcenter.capacity', 'workcenter_id', string='Product Capacities',
|
||||
help="Specific number of pieces that can be produced in parallel per product.", copy=True)
|
||||
kanban_dashboard_graph = fields.Text(compute='_compute_kanban_dashboard_graph')
|
||||
resource_calendar_id = fields.Many2one(check_company=True)
|
||||
|
||||
def _compute_display_name(self):
|
||||
super()._compute_display_name()
|
||||
for workcenter in self:
|
||||
# Show the red icon(workcenter is blocked) only when the Gantt view is accessed from MRP > Planning > Planning by Workcenter.
|
||||
if self.env.context.get('group_by') and self.env.context.get('show_workcenter_status') and workcenter.working_state == 'blocked':
|
||||
workcenter.display_name = f"{workcenter.display_name}\u00A0\u00A0🔴"
|
||||
|
||||
@api.constrains('alternative_workcenter_ids')
|
||||
def _check_alternative_workcenter(self):
|
||||
|
|
@ -81,28 +93,99 @@ class MrpWorkcenter(models.Model):
|
|||
if workcenter in workcenter.alternative_workcenter_ids:
|
||||
raise ValidationError(_("Workcenter %s cannot be an alternative of itself.", workcenter.name))
|
||||
|
||||
@api.depends('order_ids.duration_expected', 'order_ids.workcenter_id', 'order_ids.state', 'order_ids.date_planned_start')
|
||||
def _compute_kanban_dashboard_graph(self):
|
||||
week_range, date_start, date_stop = self._get_week_range_and_first_last_days()
|
||||
load_data = self._get_workcenter_load_per_week(week_range, date_start, date_stop)
|
||||
load_graph_data = self._prepare_graph_data(load_data, week_range)
|
||||
for wc in self:
|
||||
wc.kanban_dashboard_graph = json.dumps(load_graph_data[wc.id])
|
||||
|
||||
def _get_week_range_and_first_last_days(self):
|
||||
""" We calculate the delta between today and the previous monday,
|
||||
then add it to the delta between monday and the previous first day
|
||||
of the week as configured in the language settings.
|
||||
We use the result to calculate the modulo of 7 to make sure that
|
||||
we do not take the previous first day of the week from 2 weeks ago.
|
||||
|
||||
E.g. today is Thursday, the first of a week is a Tuesday.
|
||||
The delta between today and Monday is 3 days.
|
||||
The delta between Monday and the previous Tuesday is 6 days.
|
||||
(3 + 6) % 7 = 2, so from today, the first day of the current week is 2 days ago.
|
||||
"""
|
||||
week_range = {}
|
||||
locale = get_lang(self.env).code
|
||||
today = datetime.today()
|
||||
delta_from_monday_to_today = (today - start_of(today, 'week')).days
|
||||
first_week_day = int(get_lang(self.env).week_start) - 1
|
||||
day_offset = ((7 - first_week_day) + delta_from_monday_to_today) % 7
|
||||
|
||||
for delta in range(-7, 28, 7):
|
||||
week_start = start_of(today + relativedelta.relativedelta(days=delta - day_offset), 'day')
|
||||
week_end = week_start + relativedelta.relativedelta(days=6)
|
||||
short_name = (format_date(week_start, 'd - ', locale=locale)
|
||||
+ format_date(week_end, 'd MMM', locale=locale))
|
||||
if not delta:
|
||||
short_name = _('This Week')
|
||||
week_range[week_start] = short_name
|
||||
date_start = start_of(today + relativedelta.relativedelta(days=-7 - day_offset), 'day')
|
||||
date_stop = end_of(today + relativedelta.relativedelta(days=27 - day_offset), 'day')
|
||||
return week_range, date_start, date_stop
|
||||
|
||||
def _get_workcenter_load_per_week(self, week_range, date_start, date_stop):
|
||||
load_data = {rec: {} for rec in self}
|
||||
# demo data
|
||||
has_workorder = self.env['mrp.workorder'].search_count([('workcenter_id', 'in', self.ids)], limit=1)
|
||||
if not has_workorder:
|
||||
for wc in self:
|
||||
load_limit = 40 # default max load per week is 40 hours on a new workcenter
|
||||
load_data[wc] = {week_start: randint(0, int(load_limit * 2)) for week_start in week_range}
|
||||
return load_data
|
||||
|
||||
result = self.env['mrp.workorder']._read_group(
|
||||
[('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'waiting', 'ready', 'progress')),
|
||||
('production_date', '>=', date_start), ('production_date', '<=', date_stop)],
|
||||
['workcenter_id', 'production_date:week'], ['duration_expected:sum'])
|
||||
for r in result:
|
||||
load_in_hours = round(r[2] / 60, 1)
|
||||
load_data[r[0]].update({r[1]: load_in_hours})
|
||||
return load_data
|
||||
|
||||
def _prepare_graph_data(self, load_data, week_range):
|
||||
graph_data = {wid: [] for wid in self._ids}
|
||||
has_workorder = self.env['mrp.workorder'].search_count([('workcenter_id', 'in', self.ids)], limit=1)
|
||||
for workcenter in self:
|
||||
load_limit = sum(workcenter.resource_calendar_id.attendance_ids.mapped('duration_hours'))
|
||||
wc_data = {'is_sample_data': not has_workorder, 'labels': list(week_range.values())}
|
||||
load_bar = []
|
||||
excess_bar = []
|
||||
for week_start in week_range:
|
||||
load_bar.append(min(load_data[workcenter].get(week_start, 0), load_limit))
|
||||
excess_bar.append(max(float_round(load_data[workcenter].get(week_start, 0) - load_limit, precision_digits=1, rounding_method='HALF-UP'), 0))
|
||||
wc_data['values'] = [load_bar, load_limit, excess_bar]
|
||||
graph_data[workcenter.id].append(wc_data)
|
||||
return graph_data
|
||||
|
||||
@api.depends('order_ids.duration_expected', 'order_ids.workcenter_id', 'order_ids.state', 'order_ids.date_start')
|
||||
def _compute_workorder_count(self):
|
||||
MrpWorkorder = self.env['mrp.workorder']
|
||||
result = {wid: {} for wid in self._ids}
|
||||
result_duration_expected = {wid: 0 for wid in self._ids}
|
||||
# Count Late Workorder
|
||||
data = MrpWorkorder._read_group(
|
||||
[('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'waiting', 'ready')), ('date_planned_start', '<', datetime.now().strftime('%Y-%m-%d'))],
|
||||
['workcenter_id'], ['workcenter_id'])
|
||||
count_data = dict((item['workcenter_id'][0], item['workcenter_id_count']) for item in data)
|
||||
[('workcenter_id', 'in', self.ids), ('state', 'in', ('blocked', 'ready')), ('date_start', '<', datetime.now().strftime('%Y-%m-%d'))],
|
||||
['workcenter_id'], ['__count'])
|
||||
count_data = {workcenter.id: count for workcenter, count in data}
|
||||
# Count All, Pending, Ready, Progress Workorder
|
||||
res = MrpWorkorder._read_group(
|
||||
[('workcenter_id', 'in', self.ids)],
|
||||
['workcenter_id', 'state', 'duration_expected'], ['workcenter_id', 'state'],
|
||||
lazy=False)
|
||||
for res_group in res:
|
||||
result[res_group['workcenter_id'][0]][res_group['state']] = res_group['__count']
|
||||
if res_group['state'] in ('pending', 'waiting', 'ready', 'progress'):
|
||||
result_duration_expected[res_group['workcenter_id'][0]] += res_group['duration_expected']
|
||||
['workcenter_id', 'state'], ['duration_expected:sum', '__count'])
|
||||
for workcenter, state, duration_sum, count in res:
|
||||
result[workcenter.id][state] = count
|
||||
if state in ('blocked', 'ready', 'progress'):
|
||||
result_duration_expected[workcenter.id] += duration_sum
|
||||
for workcenter in self:
|
||||
workcenter.workorder_count = sum(count for state, count in result[workcenter.id].items() if state not in ('done', 'cancel'))
|
||||
workcenter.workorder_pending_count = result[workcenter.id].get('pending', 0)
|
||||
workcenter.workorder_blocked_count = result[workcenter.id].get('blocked', 0)
|
||||
workcenter.workcenter_load = result_duration_expected[workcenter.id]
|
||||
workcenter.workorder_ready_count = result[workcenter.id].get('ready', 0)
|
||||
workcenter.workorder_progress_count = result[workcenter.id].get('progress', 0)
|
||||
|
|
@ -110,14 +193,20 @@ class MrpWorkcenter(models.Model):
|
|||
|
||||
@api.depends('time_ids', 'time_ids.date_end', 'time_ids.loss_type')
|
||||
def _compute_working_state(self):
|
||||
# We search for a productivity line associated to this workcenter having no `date_end`.
|
||||
# If we do not find one, the workcenter is not currently being used. If we find one, according
|
||||
# to its `type_loss`, the workcenter is either being used or blocked.
|
||||
time_log_by_workcenter = {}
|
||||
for time_log in self.env['mrp.workcenter.productivity'].search([
|
||||
('workcenter_id', 'in', self.ids),
|
||||
('date_end', '=', False),
|
||||
]):
|
||||
wc = time_log.workcenter_id
|
||||
if wc not in time_log_by_workcenter:
|
||||
time_log_by_workcenter[wc] = time_log
|
||||
|
||||
for workcenter in self:
|
||||
# We search for a productivity line associated to this workcenter having no `date_end`.
|
||||
# If we do not find one, the workcenter is not currently being used. If we find one, according
|
||||
# to its `type_loss`, the workcenter is either being used or blocked.
|
||||
time_log = self.env['mrp.workcenter.productivity'].search([
|
||||
('workcenter_id', '=', workcenter.id),
|
||||
('date_end', '=', False)
|
||||
], limit=1)
|
||||
time_log = time_log_by_workcenter.get(workcenter._origin)
|
||||
if not time_log:
|
||||
# the workcenter is not being used
|
||||
workcenter.working_state = 'normal'
|
||||
|
|
@ -135,8 +224,8 @@ class MrpWorkcenter(models.Model):
|
|||
('workcenter_id', 'in', self.ids),
|
||||
('date_end', '!=', False),
|
||||
('loss_type', '!=', 'productive')],
|
||||
['duration', 'workcenter_id'], ['workcenter_id'], lazy=False)
|
||||
count_data = dict((item['workcenter_id'][0], item['duration']) for item in data)
|
||||
['workcenter_id'], ['duration:sum'])
|
||||
count_data = {workcenter.id: duration for workcenter, duration in data}
|
||||
for workcenter in self:
|
||||
workcenter.blocked_time = count_data.get(workcenter.id, 0.0) / 60.0
|
||||
|
||||
|
|
@ -147,36 +236,53 @@ class MrpWorkcenter(models.Model):
|
|||
('workcenter_id', 'in', self.ids),
|
||||
('date_end', '!=', False),
|
||||
('loss_type', '=', 'productive')],
|
||||
['duration', 'workcenter_id'], ['workcenter_id'], lazy=False)
|
||||
count_data = dict((item['workcenter_id'][0], item['duration']) for item in data)
|
||||
['workcenter_id'], ['duration:sum'])
|
||||
count_data = {workcenter.id: duration for workcenter, duration in data}
|
||||
for workcenter in self:
|
||||
workcenter.productive_time = count_data.get(workcenter.id, 0.0) / 60.0
|
||||
|
||||
@api.depends('blocked_time', 'productive_time')
|
||||
def _compute_oee(self):
|
||||
for order in self:
|
||||
if order.productive_time:
|
||||
order.oee = round(order.productive_time * 100.0 / (order.productive_time + order.blocked_time), 2)
|
||||
time_data = self.env['mrp.workcenter.productivity']._read_group(
|
||||
domain=[
|
||||
('date_start', '>=', fields.Datetime.to_string(datetime.now() - relativedelta.relativedelta(months=1))),
|
||||
('workcenter_id', 'in', self.ids),
|
||||
('date_end', '!=', False),
|
||||
],
|
||||
groupby=['workcenter_id', 'loss_type'],
|
||||
aggregates=['duration:sum'],
|
||||
)
|
||||
time_by_workcenter = defaultdict(lambda: {'productive_time': 0.0, 'blocked_time': 0.0})
|
||||
for data in time_data:
|
||||
workcenter, loss_type, duration = data
|
||||
time_to_update = 'productive_time' if loss_type == 'productive' else 'blocked_time'
|
||||
time_by_workcenter[workcenter.id][time_to_update] += duration
|
||||
for workcenter in self:
|
||||
workcenter_time = time_by_workcenter[workcenter.id]
|
||||
productive_time = workcenter_time['productive_time']
|
||||
if productive_time:
|
||||
blocked_time = workcenter_time['blocked_time']
|
||||
workcenter.oee = float_round(productive_time * 100.0 / (productive_time + blocked_time), precision_digits=2)
|
||||
else:
|
||||
order.oee = 0.0
|
||||
workcenter.oee = 0.0
|
||||
|
||||
def _compute_performance(self):
|
||||
wo_data = self.env['mrp.workorder']._read_group([
|
||||
('date_start', '>=', fields.Datetime.to_string(datetime.now() - relativedelta.relativedelta(months=1))),
|
||||
('workcenter_id', 'in', self.ids),
|
||||
('state', '=', 'done')], ['duration_expected', 'workcenter_id', 'duration'], ['workcenter_id'], lazy=False)
|
||||
duration_expected = dict((data['workcenter_id'][0], data['duration_expected']) for data in wo_data)
|
||||
duration = dict((data['workcenter_id'][0], data['duration']) for data in wo_data)
|
||||
('state', '=', 'done')], ['workcenter_id'], ['duration_expected:sum', 'duration:sum'])
|
||||
duration_expected = {workcenter.id: expected for workcenter, expected, __ in wo_data}
|
||||
duration = {workcenter.id: duration for workcenter, __, duration in wo_data}
|
||||
for workcenter in self:
|
||||
if duration.get(workcenter.id):
|
||||
workcenter.performance = 100 * duration_expected.get(workcenter.id, 0.0) / duration[workcenter.id]
|
||||
else:
|
||||
workcenter.performance = 0.0
|
||||
|
||||
@api.constrains('default_capacity')
|
||||
def _check_capacity(self):
|
||||
if any(workcenter.default_capacity <= 0.0 for workcenter in self):
|
||||
raise exceptions.UserError(_('The capacity must be strictly positive.'))
|
||||
@api.depends('routing_line_ids')
|
||||
def _compute_has_routing_lines(self):
|
||||
for workcenter in self:
|
||||
workcenter.has_routing_lines = self.env['mrp.routing.workcenter'].search_count([('workcenter_id', 'in', workcenter.ids)], limit=1)
|
||||
|
||||
def unblock(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -184,7 +290,7 @@ class MrpWorkcenter(models.Model):
|
|||
raise exceptions.UserError(_("It has already been unblocked."))
|
||||
times = self.env['mrp.workcenter.productivity'].search([('workcenter_id', '=', self.id), ('date_end', '=', False)])
|
||||
times.write({'date_end': datetime.now()})
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
return True
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
@ -211,6 +317,12 @@ class MrpWorkcenter(models.Model):
|
|||
action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_work_orders")
|
||||
return action
|
||||
|
||||
def action_work_order_alternatives(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_workorder_todo")
|
||||
action['domain'] = ['|', ('workcenter_id', 'in', self.alternative_workcenter_ids.ids),
|
||||
('workcenter_id.alternative_workcenter_ids', '=', self.id)]
|
||||
return action
|
||||
|
||||
def _get_unavailability_intervals(self, start_datetime, end_datetime):
|
||||
"""Get the unavailabilities intervals for the workcenters in `self`.
|
||||
|
||||
|
|
@ -224,7 +336,7 @@ class MrpWorkcenter(models.Model):
|
|||
unavailability_ressources = self.resource_id._get_unavailable_intervals(start_datetime, end_datetime)
|
||||
return {wc.id: unavailability_ressources.get(wc.resource_id.id, []) for wc in self}
|
||||
|
||||
def _get_first_available_slot(self, start_datetime, duration):
|
||||
def _get_first_available_slot(self, start_datetime, duration, forward=True, leaves_to_ignore=False, extra_leaves_slots=[]):
|
||||
"""Get the first available interval for the workcenter in `self`.
|
||||
|
||||
The available interval is disjoinct with all other workorders planned on this workcenter, but
|
||||
|
|
@ -232,44 +344,68 @@ class MrpWorkcenter(models.Model):
|
|||
Return the first available interval (start datetime, end datetime) or,
|
||||
if there is none before 700 days, a tuple error (False, 'error message').
|
||||
|
||||
:param start_datetime: begin the search at this datetime
|
||||
:param duration: minutes needed to make the workorder (float)
|
||||
:param start_datetime: begin the search at this datetime
|
||||
:param forward: forward scheduling (search from start_datetime to 700 days after), or backward (from start_datetime to now)
|
||||
:param leaves_to_ignore: typically, ignore allocated leave when re-planning a workorder
|
||||
:param extra_leaves_slots: extra time slots (start, stop) to consider
|
||||
:rtype: tuple
|
||||
"""
|
||||
self.ensure_one()
|
||||
start_datetime, revert = make_aware(start_datetime)
|
||||
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
max_planning_iterations = max(int(ICP.get_param('mrp.workcenter_max_planning_iterations', '50')), 1)
|
||||
resource = self.resource_id
|
||||
get_available_intervals = partial(self.resource_calendar_id._work_intervals_batch, domain=[('time_type', 'in', ['other', 'leave'])], resources=resource, tz=timezone(self.resource_calendar_id.tz))
|
||||
get_workorder_intervals = partial(self.resource_calendar_id._leave_intervals_batch, domain=[('time_type', '=', 'other')], resources=resource, tz=timezone(self.resource_calendar_id.tz))
|
||||
revert = to_timezone(start_datetime.tzinfo)
|
||||
start_datetime = localized(start_datetime)
|
||||
get_available_intervals = partial(self.resource_calendar_id._work_intervals_batch, resources=resource, tz=timezone(self.resource_calendar_id.tz))
|
||||
workorder_intervals_leaves_domain = [('time_type', '=', 'other')]
|
||||
if leaves_to_ignore:
|
||||
workorder_intervals_leaves_domain.append(('id', 'not in', leaves_to_ignore.ids))
|
||||
get_workorder_intervals = partial(self.resource_calendar_id._leave_intervals_batch, domain=workorder_intervals_leaves_domain, resources=resource, tz=timezone(self.resource_calendar_id.tz))
|
||||
extra_leaves_slots_intervals = Intervals([(localized(start), localized(stop), self.env['resource.calendar.attendance']) for start, stop in extra_leaves_slots])
|
||||
|
||||
remaining = duration
|
||||
start_interval = start_datetime
|
||||
remaining = duration = max(duration, 1 / 60)
|
||||
now = localized(datetime.now())
|
||||
delta = timedelta(days=14)
|
||||
|
||||
for n in range(50): # 50 * 14 = 700 days in advance (hardcoded)
|
||||
dt = start_datetime + delta * n
|
||||
available_intervals = get_available_intervals(dt, dt + delta)[resource.id]
|
||||
workorder_intervals = get_workorder_intervals(dt, dt + delta)[resource.id]
|
||||
for start, stop, dummy in available_intervals:
|
||||
# Shouldn't loop more than 2 times because the available_intervals contains the workorder_intervals
|
||||
# And remaining == duration can only occur at the first loop and at the interval intersection (cannot happen several time because available_intervals > workorder_intervals
|
||||
for _i in range(2):
|
||||
start_interval, stop_interval = None, None
|
||||
for n in range(max_planning_iterations): # 50 * 14 = 700 days in advance
|
||||
if forward:
|
||||
date_start = start_datetime + delta * n
|
||||
date_stop = date_start + delta
|
||||
available_intervals = get_available_intervals(date_start, date_stop)[resource.id]
|
||||
workorder_intervals = get_workorder_intervals(date_start, date_stop)[resource.id]
|
||||
for start, stop, _records in available_intervals:
|
||||
start_interval = start_interval or start
|
||||
interval_minutes = (stop - start).total_seconds() / 60
|
||||
# If the remaining minutes has never decrease update start_interval
|
||||
if remaining == duration:
|
||||
start_interval = start
|
||||
# If there is a overlap between the possible available interval and a others WO
|
||||
if Intervals([(start_interval, start + timedelta(minutes=min(remaining, interval_minutes)), dummy)]) & workorder_intervals:
|
||||
remaining = duration
|
||||
elif float_compare(interval_minutes, remaining, precision_digits=3) >= 0:
|
||||
while (interval := Intervals([(start_interval or start, start + timedelta(minutes=min(remaining, interval_minutes)), _records)])) \
|
||||
and (conflict := interval & workorder_intervals or interval & extra_leaves_slots_intervals):
|
||||
(_start, start, _records) = conflict._items[0] # restart available interval at conflicting interval stop
|
||||
interval_minutes = (stop - start).total_seconds() / 60
|
||||
start_interval, remaining = start if interval_minutes else None, duration
|
||||
if float_compare(interval_minutes, remaining, precision_digits=3) >= 0:
|
||||
return revert(start_interval), revert(start + timedelta(minutes=remaining))
|
||||
else:
|
||||
# Decrease a part of the remaining duration
|
||||
remaining -= interval_minutes
|
||||
# Go to the next available interval because the possible current interval duration has been used
|
||||
break
|
||||
return False, 'Not available slot 700 days after the planned start'
|
||||
remaining -= interval_minutes
|
||||
else:
|
||||
# same process but starting from end on reversed intervals
|
||||
date_stop = start_datetime - delta * n
|
||||
date_start = date_stop - delta
|
||||
available_intervals = get_available_intervals(date_start, date_stop)[resource.id]
|
||||
available_intervals = reversed(available_intervals)
|
||||
workorder_intervals = get_workorder_intervals(date_start, date_stop)[resource.id]
|
||||
for start, stop, _records in available_intervals:
|
||||
stop_interval = stop_interval or stop
|
||||
interval_minutes = (stop - start).total_seconds() / 60
|
||||
while (interval := Intervals([(stop - timedelta(minutes=min(remaining, interval_minutes)), stop_interval or stop, _records)])) \
|
||||
and (conflict := interval & workorder_intervals or interval & extra_leaves_slots_intervals):
|
||||
(stop, _stop, _records) = conflict._items[0] # restart available interval at conflicting interval start
|
||||
interval_minutes = (stop - start).total_seconds() / 60
|
||||
stop_interval, remaining = stop if interval_minutes else None, duration
|
||||
if float_compare(interval_minutes, remaining, precision_digits=3) >= 0:
|
||||
return revert(stop - timedelta(minutes=remaining)), revert(stop_interval)
|
||||
remaining -= interval_minutes
|
||||
if date_start <= now:
|
||||
break
|
||||
return False, 'No available slot 700 days after the planned start'
|
||||
|
||||
def action_archive(self):
|
||||
res = super().action_archive()
|
||||
|
|
@ -288,21 +424,20 @@ class MrpWorkcenter(models.Model):
|
|||
}
|
||||
return res
|
||||
|
||||
def _get_capacity(self, product):
|
||||
product_capacity = self.capacity_ids.filtered(lambda capacity: capacity.product_id == product)
|
||||
return product_capacity.capacity if product_capacity else self.default_capacity
|
||||
|
||||
def _get_expected_duration(self, product_id):
|
||||
"""Compute the expected duration when using this work-center
|
||||
Always include workcenter startup time and clean-up time.
|
||||
In case there are specific capacities defined in the workcenter
|
||||
that matches the product we are producing. Add the extra-time.
|
||||
"""
|
||||
capacity = self.capacity_ids.filtered(lambda p: p.product_id == product_id)
|
||||
return self.time_start + self.time_stop + (capacity.time_start + capacity.time_stop if capacity else 0.0)
|
||||
def _get_capacity(self, product, unit, default_capacity=1):
|
||||
capacity = self.capacity_ids.sorted(lambda c: (
|
||||
not (c.product_id == product and c.product_uom_id == product.uom_id),
|
||||
not (not c.product_id and c.product_uom_id == unit),
|
||||
not (not c.product_id and c.product_uom_id == product.uom_id),
|
||||
))[:1]
|
||||
if capacity and capacity.product_id in [product, self.env['product.product']] and capacity.product_uom_id in [product.uom_id, unit]:
|
||||
if float_is_zero(capacity.capacity, 0):
|
||||
return (default_capacity, capacity.time_start, capacity.time_stop)
|
||||
return (capacity.product_uom_id._compute_quantity(capacity.capacity, unit), capacity.time_start, capacity.time_stop)
|
||||
return (default_capacity, self.time_start, self.time_stop)
|
||||
|
||||
|
||||
class WorkcenterTag(models.Model):
|
||||
class MrpWorkcenterTag(models.Model):
|
||||
_name = 'mrp.workcenter.tag'
|
||||
_description = 'Add tag for the workcenter'
|
||||
_order = 'name'
|
||||
|
|
@ -313,27 +448,24 @@ class WorkcenterTag(models.Model):
|
|||
name = fields.Char("Tag Name", required=True)
|
||||
color = fields.Integer("Color Index", default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('tag_name_unique', 'unique(name)',
|
||||
'The tag name must be unique.'),
|
||||
]
|
||||
_tag_name_unique = models.Constraint(
|
||||
'unique(name)',
|
||||
'The tag name must be unique.',
|
||||
)
|
||||
|
||||
|
||||
class MrpWorkcenterProductivityLossType(models.Model):
|
||||
_name = "mrp.workcenter.productivity.loss.type"
|
||||
_name = 'mrp.workcenter.productivity.loss.type'
|
||||
_description = 'MRP Workorder productivity losses'
|
||||
_rec_name = 'loss_type'
|
||||
|
||||
@api.depends('loss_type')
|
||||
def name_get(self):
|
||||
def _compute_display_name(self):
|
||||
""" As 'category' field in form view is a Many2one, its value will be in
|
||||
lower case. In order to display its value capitalized 'name_get' is
|
||||
lower case. In order to display its value capitalized 'display_name' is
|
||||
overrided.
|
||||
"""
|
||||
result = []
|
||||
for rec in self:
|
||||
result.append((rec.id, rec.loss_type.title()))
|
||||
return result
|
||||
rec.display_name = rec.loss_type.title()
|
||||
|
||||
loss_type = fields.Selection([
|
||||
('availability', 'Availability'),
|
||||
|
|
@ -343,15 +475,15 @@ class MrpWorkcenterProductivityLossType(models.Model):
|
|||
|
||||
|
||||
class MrpWorkcenterProductivityLoss(models.Model):
|
||||
_name = "mrp.workcenter.productivity.loss"
|
||||
_name = 'mrp.workcenter.productivity.loss'
|
||||
_description = "Workcenter Productivity Losses"
|
||||
_order = "sequence, id"
|
||||
|
||||
name = fields.Char('Blocking Reason', required=True)
|
||||
name = fields.Char('Blocking Reason', required=True, translate=True)
|
||||
sequence = fields.Integer('Sequence', default=1)
|
||||
manual = fields.Boolean('Is a Blocking Reason', default=True)
|
||||
loss_id = fields.Many2one('mrp.workcenter.productivity.loss.type', domain=([('loss_type', 'in', ['quality', 'availability'])]), string='Category')
|
||||
loss_type = fields.Selection(string='Effectiveness Category', related='loss_id.loss_type', store=True, readonly=False)
|
||||
loss_id = fields.Many2one('mrp.workcenter.productivity.loss.type', domain=[('loss_type', 'in', ['quality', 'availability'])], string='Category')
|
||||
loss_type = fields.Selection(string='Effectiveness Category', related='loss_id.loss_type', readonly=False)
|
||||
|
||||
def _convert_to_duration(self, date_start, date_stop, workcenter=False):
|
||||
""" Convert a date range into a duration in minutes.
|
||||
|
|
@ -368,8 +500,9 @@ class MrpWorkcenterProductivityLoss(models.Model):
|
|||
duration = max(duration, (date_stop - date_start).total_seconds() / 60.0)
|
||||
return round(duration, 2)
|
||||
|
||||
|
||||
class MrpWorkcenterProductivity(models.Model):
|
||||
_name = "mrp.workcenter.productivity"
|
||||
_name = 'mrp.workcenter.productivity'
|
||||
_description = "Workcenter Productivity Log"
|
||||
_order = "id desc"
|
||||
_rec_name = "loss_id"
|
||||
|
|
@ -402,7 +535,7 @@ class MrpWorkcenterProductivity(models.Model):
|
|||
'mrp.workcenter.productivity.loss', "Loss Reason",
|
||||
ondelete='restrict', required=True)
|
||||
loss_type = fields.Selection(
|
||||
string="Effectiveness", related='loss_id.loss_type', store=True, readonly=False)
|
||||
string="Effectiveness", related='loss_id.loss_type', readonly=False)
|
||||
description = fields.Text('Description')
|
||||
date_start = fields.Datetime('Start Date', default=fields.Datetime.now, required=True)
|
||||
date_end = fields.Datetime('End Date')
|
||||
|
|
@ -416,17 +549,48 @@ class MrpWorkcenterProductivity(models.Model):
|
|||
else:
|
||||
blocktime.duration = 0.0
|
||||
|
||||
@api.onchange('duration')
|
||||
def _duration_changed(self):
|
||||
if not self.date_end:
|
||||
return
|
||||
self.date_start = self.date_end - timedelta(minutes=self.duration)
|
||||
self._loss_type_change()
|
||||
|
||||
@api.onchange('date_start')
|
||||
def _date_start_changed(self):
|
||||
if not self.date_start:
|
||||
return
|
||||
self.date_end = self.date_start + timedelta(minutes=self.duration)
|
||||
self._loss_type_change()
|
||||
|
||||
@api.onchange('date_end')
|
||||
def _date_end_changed(self):
|
||||
if not self.date_end:
|
||||
return
|
||||
self.date_start = self.date_end - timedelta(minutes=self.duration)
|
||||
self._loss_type_change()
|
||||
|
||||
@api.constrains('workorder_id')
|
||||
def _check_open_time_ids(self):
|
||||
for workorder in self.workorder_id:
|
||||
open_time_ids_by_user = self.env["mrp.workcenter.productivity"].read_group([("id", "in", workorder.time_ids.ids), ("date_end", "=", False)], ["user_id", "open_time_ids_count:count(id)"], ["user_id"])
|
||||
if any(data["open_time_ids_count"] > 1 for data in open_time_ids_by_user):
|
||||
open_time_ids_by_user = self.env["mrp.workcenter.productivity"]._read_group(
|
||||
[("id", "in", workorder.time_ids.ids), ("date_end", "=", False)],
|
||||
["user_id"], having=[("__count", ">", 1)],
|
||||
)
|
||||
if open_time_ids_by_user:
|
||||
raise ValidationError(_('The Workorder (%s) cannot be started twice!', workorder.display_name))
|
||||
|
||||
def button_block(self):
|
||||
self.ensure_one()
|
||||
self.workcenter_id.order_ids.end_all()
|
||||
|
||||
def _loss_type_change(self):
|
||||
self.ensure_one()
|
||||
if self.workorder_id.duration > self.workorder_id.duration_expected:
|
||||
self.loss_id = self.env.ref("mrp.block_reason4").id
|
||||
else:
|
||||
self.loss_id = self.env.ref("mrp.block_reason7").id
|
||||
|
||||
def _close(self):
|
||||
underperformance_timers = self.env['mrp.workcenter.productivity']
|
||||
for timer in self:
|
||||
|
|
@ -446,19 +610,37 @@ class MrpWorkcenterProductivity(models.Model):
|
|||
underperformance_timers.write({'loss_id': underperformance_type.id})
|
||||
|
||||
|
||||
class MrpWorkCenterCapacity(models.Model):
|
||||
class MrpWorkcenterCapacity(models.Model):
|
||||
_name = 'mrp.workcenter.capacity'
|
||||
_description = 'Work Center Capacity'
|
||||
_check_company_auto = True
|
||||
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', string='Work Center', required=True)
|
||||
product_id = fields.Many2one('product.product', string='Product', required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Product UoM', related='product_id.uom_id')
|
||||
capacity = fields.Float('Capacity', default=1.0, help="Number of pieces that can be produced in parallel for this product.")
|
||||
time_start = fields.Float('Setup Time (minutes)', help="Additional time in minutes for the setup.")
|
||||
time_stop = fields.Float('Cleanup Time (minutes)', help="Additional time in minutes for the cleaning.")
|
||||
def _default_time_start(self):
|
||||
workcenter_id = self.workcenter_id.id or self.env.context.get('default_workcenter_id')
|
||||
return self.env['mrp.workcenter'].browse(workcenter_id).time_start if workcenter_id else 0.0
|
||||
|
||||
_sql_constraints = [
|
||||
('positive_capacity', 'CHECK(capacity > 0)', 'Capacity should be a positive number.'),
|
||||
('unique_product', 'UNIQUE(workcenter_id, product_id)', 'Product capacity should be unique for each workcenter.'),
|
||||
]
|
||||
def _default_time_stop(self):
|
||||
workcenter_id = self.workcenter_id.id or self.env.context.get('default_workcenter_id')
|
||||
return self.env['mrp.workcenter'].browse(workcenter_id).time_stop if workcenter_id else 0.0
|
||||
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', string='Work Center', required=True, index=True)
|
||||
product_id = fields.Many2one('product.product', string='Product')
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unit',
|
||||
compute="_compute_product_uom_id", precompute=True, store=True, readonly=False, required=True)
|
||||
capacity = fields.Float('Capacity', help="Number of pieces that can be produced in parallel for this product or for all, depending on the unit.")
|
||||
time_start = fields.Float('Setup Time (minutes)', default=_default_time_start, help="Time in minutes for the setup.")
|
||||
time_stop = fields.Float('Cleanup Time (minutes)', default=_default_time_stop, help="Time in minutes for the cleaning.")
|
||||
|
||||
_positive_capacity = models.Constraint(
|
||||
'CHECK(capacity >= 0)',
|
||||
'Capacity should be a non-negative number.',
|
||||
)
|
||||
_workcenter_product_product_uom_unique = models.UniqueIndex(
|
||||
'(workcenter_id, COALESCE(product_id, 0), product_uom_id)',
|
||||
'Product/Unit capacity should be unique for each workcenter.'
|
||||
)
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_uom_id(self):
|
||||
for capacity in self:
|
||||
capacity.product_uom_id = capacity.product_id.uom_id or self.env.ref('uom.product_uom_unit')
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import collections
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
import operator as py_operator
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import groupby
|
||||
from odoo.tools.float_utils import float_round, float_is_zero
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
OPERATORS = {
|
||||
PY_OPERATORS = {
|
||||
'<': py_operator.lt,
|
||||
'>': py_operator.gt,
|
||||
'<=': py_operator.le,
|
||||
'>=': py_operator.ge,
|
||||
'=': py_operator.eq,
|
||||
'!=': py_operator.ne
|
||||
'!=': py_operator.ne,
|
||||
'in': lambda elem, container: elem in container,
|
||||
'not in': lambda elem, container: elem not in container,
|
||||
}
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
|
|
@ -28,42 +28,31 @@ class ProductTemplate(models.Model):
|
|||
compute='_compute_bom_count', compute_sudo=False)
|
||||
used_in_bom_count = fields.Integer('# of BoM Where is Used',
|
||||
compute='_compute_used_in_bom_count', compute_sudo=False)
|
||||
mrp_product_qty = fields.Float('Manufactured', digits='Product Unit of Measure',
|
||||
mrp_product_qty = fields.Float('Manufactured', digits='Product Unit',
|
||||
compute='_compute_mrp_product_qty', compute_sudo=False)
|
||||
produce_delay = fields.Float(
|
||||
'Manufacturing Lead Time', default=0.0,
|
||||
help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.")
|
||||
is_kits = fields.Boolean(compute='_compute_is_kits', search='_search_is_kits')
|
||||
days_to_prepare_mo = fields.Float(
|
||||
string="Days to prepare Manufacturing Order", default=0.0,
|
||||
help="Create and confirm Manufacturing Orders this many days in advance, to have enough time to replenish components or manufacture semi-finished products.\n"
|
||||
"Note that security lead times will also be considered when appropriate.")
|
||||
|
||||
def _compute_bom_count(self):
|
||||
for product in self:
|
||||
product.bom_count = self.env['mrp.bom'].search_count(['|', ('product_tmpl_id', '=', product.id), ('byproduct_ids.product_id.product_tmpl_id', '=', product.id)])
|
||||
product.bom_count = self.env['mrp.bom'].search_count(
|
||||
['|', ('product_tmpl_id', 'in', product.ids), ('byproduct_ids.product_id.product_tmpl_id', 'in', product.ids)]
|
||||
)
|
||||
|
||||
@api.depends_context('company')
|
||||
def _compute_is_kits(self):
|
||||
domain = [('company_id', 'in', [False, self.env.company.id]),
|
||||
('product_tmpl_id', 'in', self.ids),
|
||||
('active', '=', True),
|
||||
('type', '=', 'phantom')]
|
||||
bom_mapping = self.env['mrp.bom'].sudo()._read_group(
|
||||
domain, ['product_tmpl_id'], ['product_tmpl_id'], orderby='id')
|
||||
kits_ids = {b['product_tmpl_id'][0] for b in bom_mapping}
|
||||
domain = [('product_tmpl_id', 'in', self.ids), ('type', '=', 'phantom'), '|', ('company_id', '=', False), ('company_id', '=', self.env.company.id)]
|
||||
bom_mapping = self.env['mrp.bom'].sudo().search_read(domain, ['product_tmpl_id'])
|
||||
kits_ids = set(b['product_tmpl_id'][0] for b in bom_mapping)
|
||||
for template in self:
|
||||
template.is_kits = (template.id in kits_ids)
|
||||
|
||||
def _search_is_kits(self, operator, value):
|
||||
assert operator in ('=', '!='), 'Unsupported operator'
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
bom_tmpl_query = self.env['mrp.bom'].sudo()._search(
|
||||
[('company_id', 'in', [False] + self.env.companies.ids),
|
||||
('type', '=', 'phantom'), ('active', '=', True)])
|
||||
neg = ''
|
||||
if (operator == '=' and not value) or (operator == '!=' and value):
|
||||
neg = 'not '
|
||||
return [('id', neg + 'inselect', bom_tmpl_query.subselect('product_tmpl_id'))]
|
||||
return [('id', 'in', bom_tmpl_query.subselect('product_tmpl_id'))]
|
||||
|
||||
def _compute_show_qty_status_button(self):
|
||||
super()._compute_show_qty_status_button()
|
||||
|
|
@ -72,17 +61,20 @@ class ProductTemplate(models.Model):
|
|||
template.show_on_hand_qty_status_button = template.product_variant_count <= 1
|
||||
template.show_forecasted_qty_status_button = False
|
||||
|
||||
def _should_open_product_quants(self):
|
||||
return super()._should_open_product_quants() or self.is_kits
|
||||
|
||||
def _compute_used_in_bom_count(self):
|
||||
for template in self:
|
||||
template.used_in_bom_count = self.env['mrp.bom'].search_count(
|
||||
[('bom_line_ids.product_tmpl_id', '=', template.id)])
|
||||
[('bom_line_ids.product_tmpl_id', 'in', template.ids)])
|
||||
|
||||
def write(self, values):
|
||||
if 'active' in values:
|
||||
self.filtered(lambda p: p.active != values['active']).with_context(active_test=False).bom_ids.write({
|
||||
'active': values['active']
|
||||
def write(self, vals):
|
||||
if 'active' in vals:
|
||||
self.filtered(lambda p: p.active != vals['active']).with_context(active_test=False).bom_ids.write({
|
||||
'active': vals['active']
|
||||
})
|
||||
return super().write(values)
|
||||
return super().write(vals)
|
||||
|
||||
def action_used_in_bom(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -92,22 +84,16 @@ class ProductTemplate(models.Model):
|
|||
|
||||
def _compute_mrp_product_qty(self):
|
||||
for template in self:
|
||||
template.mrp_product_qty = float_round(sum(template.mapped('product_variant_ids').mapped('mrp_product_qty')), precision_rounding=template.uom_id.rounding)
|
||||
template.mrp_product_qty = template.uom_id.round(sum(template.mapped('product_variant_ids').mapped('mrp_product_qty')))
|
||||
|
||||
def action_view_mos(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_production_report")
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_production_action")
|
||||
action['domain'] = [('state', '=', 'done'), ('product_tmpl_id', 'in', self.ids)]
|
||||
action['context'] = {
|
||||
'graph_measure': 'product_uom_qty',
|
||||
'search_default_filter_plan_date': 1,
|
||||
}
|
||||
return action
|
||||
|
||||
def action_compute_bom_days(self):
|
||||
templates = self.filtered(lambda t: t.bom_count > 0)
|
||||
if templates:
|
||||
return templates.mapped('product_variant_id').action_compute_bom_days()
|
||||
|
||||
def action_archive(self):
|
||||
filtered_products = self.env['mrp.bom.line'].search([('product_id', 'in', self.product_variant_ids.ids), ('bom_id.active', '=', True)]).product_id.mapped('display_name')
|
||||
res = super().action_archive()
|
||||
|
|
@ -125,6 +111,10 @@ class ProductTemplate(models.Model):
|
|||
}
|
||||
return res
|
||||
|
||||
def _get_backend_root_menu_ids(self):
|
||||
return super()._get_backend_root_menu_ids() + [self.env.ref('mrp.menu_mrp_root').id]
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
|
|
@ -134,34 +124,50 @@ class ProductProduct(models.Model):
|
|||
compute='_compute_bom_count', compute_sudo=False)
|
||||
used_in_bom_count = fields.Integer('# BoM Where Used',
|
||||
compute='_compute_used_in_bom_count', compute_sudo=False)
|
||||
mrp_product_qty = fields.Float('Manufactured', digits='Product Unit of Measure',
|
||||
mrp_product_qty = fields.Float('Manufactured', digits='Product Unit',
|
||||
compute='_compute_mrp_product_qty', compute_sudo=False)
|
||||
is_kits = fields.Boolean(compute="_compute_is_kits", search='_search_is_kits')
|
||||
|
||||
# Catalog related fields
|
||||
product_catalog_product_is_in_bom = fields.Boolean(
|
||||
compute='_compute_product_is_in_bom_and_mo',
|
||||
search='_search_product_is_in_bom',
|
||||
)
|
||||
|
||||
product_catalog_product_is_in_mo = fields.Boolean(
|
||||
compute='_compute_product_is_in_bom_and_mo',
|
||||
search='_search_product_is_in_mo',
|
||||
)
|
||||
|
||||
def _compute_bom_count(self):
|
||||
for product in self:
|
||||
product.bom_count = self.env['mrp.bom'].search_count(['|', '|', ('byproduct_ids.product_id', '=', product.id), ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product.product_tmpl_id.id)])
|
||||
product.bom_count = self.env['mrp.bom'].search_count([
|
||||
'|', '|', ('byproduct_ids.product_id', 'in', product.ids), ('product_id', 'in', product.ids),
|
||||
'&', ('product_id', '=', False), ('product_tmpl_id', 'in', product.product_tmpl_id.ids),
|
||||
])
|
||||
|
||||
@api.depends_context('company')
|
||||
def _compute_is_kits(self):
|
||||
domain = [
|
||||
'&', ('company_id', 'in', [False, self.env.company.id]),
|
||||
'&', ('active', '=', True),
|
||||
'&', ('type', '=', 'phantom'),
|
||||
'|', ('product_id', 'in', self.ids),
|
||||
'&', ('product_id', '=', False),
|
||||
('product_tmpl_id', 'in', self.product_tmpl_id.ids)]
|
||||
tmpl_bom_mapping = self.env['mrp.bom'].sudo()._read_group(
|
||||
domain, ['product_tmpl_id'], ['product_tmpl_id'], orderby='id')
|
||||
product_bom_mapping = self.env['mrp.bom'].sudo()._read_group(
|
||||
domain, ['product_id'], ['product_id'], orderby='id')
|
||||
kits_template_ids = {b['product_tmpl_id'][0] for b in tmpl_bom_mapping}
|
||||
kits_product_ids = {b['product_id'][0] for b in product_bom_mapping if b['product_id']}
|
||||
domain = ['&', '&', ('type', '=', 'phantom'),
|
||||
'|', ('company_id', '=', False),
|
||||
('company_id', '=', self.env.company.id),
|
||||
'|', ('product_id', 'in', self.ids),
|
||||
'&', ('product_id', '=', False),
|
||||
('product_tmpl_id', 'in', self.product_tmpl_id.ids)]
|
||||
bom_mapping = self.env['mrp.bom'].sudo().search_read(domain, ['product_tmpl_id', 'product_id'])
|
||||
kits_template_ids = set([])
|
||||
kits_product_ids = set([])
|
||||
for bom_data in bom_mapping:
|
||||
if bom_data['product_id']:
|
||||
kits_product_ids.add(bom_data['product_id'][0])
|
||||
else:
|
||||
kits_template_ids.add(bom_data['product_tmpl_id'][0])
|
||||
for product in self:
|
||||
product.is_kits = (product.id in kits_product_ids or product.product_tmpl_id.id in kits_template_ids)
|
||||
|
||||
def _search_is_kits(self, operator, value):
|
||||
assert operator in ('=', '!='), 'Unsupported operator'
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
bom_tmpl_query = self.env['mrp.bom'].sudo()._search(
|
||||
[('company_id', 'in', [False] + self.env.companies.ids),
|
||||
('active', '=', True),
|
||||
|
|
@ -169,13 +175,10 @@ class ProductProduct(models.Model):
|
|||
bom_product_query = self.env['mrp.bom'].sudo()._search(
|
||||
[('company_id', 'in', [False] + self.env.companies.ids),
|
||||
('type', '=', 'phantom'), ('product_id', '!=', False)])
|
||||
neg = ''
|
||||
op = '|'
|
||||
if (operator == '=' and not value) or (operator == '!=' and value):
|
||||
neg = 'not '
|
||||
op = '&'
|
||||
return [op, ('product_tmpl_id', neg + 'inselect', bom_tmpl_query.subselect('product_tmpl_id')),
|
||||
('id', neg + 'inselect', bom_product_query.subselect('product_id'))]
|
||||
return [
|
||||
'|', ('product_tmpl_id', 'in', bom_tmpl_query.subselect('product_tmpl_id')),
|
||||
('id', 'in', bom_product_query.subselect('product_id'))
|
||||
]
|
||||
|
||||
def _compute_show_qty_status_button(self):
|
||||
super()._compute_show_qty_status_button()
|
||||
|
|
@ -186,14 +189,44 @@ class ProductProduct(models.Model):
|
|||
|
||||
def _compute_used_in_bom_count(self):
|
||||
for product in self:
|
||||
product.used_in_bom_count = self.env['mrp.bom'].search_count([('bom_line_ids.product_id', '=', product.id)])
|
||||
product.used_in_bom_count = self.env['mrp.bom'].search_count(
|
||||
[('bom_line_ids.product_id', 'in', product.ids)])
|
||||
|
||||
def write(self, values):
|
||||
if 'active' in values:
|
||||
self.filtered(lambda p: p.active != values['active']).with_context(active_test=False).variant_bom_ids.write({
|
||||
'active': values['active']
|
||||
@api.depends_context('order_id')
|
||||
def _compute_product_is_in_bom_and_mo(self):
|
||||
# Just to enable the _search method
|
||||
self.product_catalog_product_is_in_bom = False
|
||||
self.product_catalog_product_is_in_mo = False
|
||||
|
||||
def _search_product_is_in_bom(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
product_ids = self.env['mrp.bom.line'].search([
|
||||
('bom_id', '=', self.env.context.get('order_id', '')),
|
||||
]).product_id.ids
|
||||
return [('id', operator, product_ids)]
|
||||
|
||||
def _search_product_is_in_mo(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
product_ids = self.env['mrp.production'].search([
|
||||
('id', 'in', [self.env.context.get('order_id', '')]),
|
||||
]).move_raw_ids.product_id.ids
|
||||
return [('id', operator, product_ids)]
|
||||
|
||||
def write(self, vals):
|
||||
if 'active' in vals:
|
||||
self.filtered(lambda p: p.active != vals['active']).with_context(active_test=False).variant_bom_ids.write({
|
||||
'active': vals['active']
|
||||
})
|
||||
return super().write(values)
|
||||
return super().write(vals)
|
||||
|
||||
def get_total_routes(self):
|
||||
routes = super().get_total_routes()
|
||||
if self.bom_ids:
|
||||
manufacture_routes = self.env['stock.rule'].search([('action', '=', 'manufacture')]).route_id
|
||||
routes |= manufacture_routes
|
||||
return routes
|
||||
|
||||
def get_components(self):
|
||||
""" Return the components list ids in case of kit product.
|
||||
|
|
@ -202,7 +235,7 @@ class ProductProduct(models.Model):
|
|||
bom_kit = self.env['mrp.bom']._bom_find(self, bom_type='phantom')[self]
|
||||
if bom_kit:
|
||||
boms, bom_sub_lines = bom_kit.explode(self, 1)
|
||||
return [bom_line.product_id.id for bom_line, data in bom_sub_lines if bom_line.product_id.type == 'product']
|
||||
return [bom_line.product_id.id for bom_line, data in bom_sub_lines if bom_line.product_id.is_storable]
|
||||
else:
|
||||
return super(ProductProduct, self).get_components()
|
||||
|
||||
|
|
@ -213,16 +246,16 @@ class ProductProduct(models.Model):
|
|||
return action
|
||||
|
||||
def _compute_mrp_product_qty(self):
|
||||
date_from = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=365))
|
||||
date_from = fields.Datetime.to_string(fields.Datetime.now() - timedelta(days=365))
|
||||
#TODO: state = done?
|
||||
domain = [('state', '=', 'done'), ('product_id', 'in', self.ids), ('date_planned_start', '>', date_from)]
|
||||
read_group_res = self.env['mrp.production']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id'])
|
||||
mapped_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in read_group_res])
|
||||
domain = [('state', '=', 'done'), ('product_id', 'in', self.ids), ('date_start', '>', date_from)]
|
||||
read_group_res = self.env['mrp.production']._read_group(domain, ['product_id'], ['product_uom_qty:sum'])
|
||||
mapped_data = {product.id: qty for product, qty in read_group_res}
|
||||
for product in self:
|
||||
if not product.id:
|
||||
product.mrp_product_qty = 0.0
|
||||
continue
|
||||
product.mrp_product_qty = float_round(mapped_data.get(product.id, 0), precision_rounding=product.uom_id.rounding)
|
||||
product.mrp_product_qty = product.uom_id.round(mapped_data.get(product.id, 0))
|
||||
|
||||
def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False, to_date=False):
|
||||
""" When the product is a kit, this override computes the fields :
|
||||
|
|
@ -271,7 +304,7 @@ class ProductProduct(models.Model):
|
|||
component = component.with_context(mrp_compute_quantities=qties).with_prefetch(prefetch_component_ids)
|
||||
qty_per_kit = 0
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
|
||||
if not component.is_storable or bom_line.product_uom_id.is_zero(bom_line_data['qty']):
|
||||
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
|
||||
# to avoid a division by zero. The same logic is applied to non-storable products as those
|
||||
# products have 0 qty available.
|
||||
|
|
@ -280,30 +313,29 @@ class ProductProduct(models.Model):
|
|||
qty_per_kit += bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False)
|
||||
if not qty_per_kit:
|
||||
continue
|
||||
rounding = component.uom_id.rounding
|
||||
component_res = (
|
||||
qties.get(component.id)
|
||||
if component.id in qties
|
||||
else {
|
||||
"virtual_available": float_round(component.virtual_available, precision_rounding=rounding),
|
||||
"qty_available": float_round(component.qty_available, precision_rounding=rounding),
|
||||
"incoming_qty": float_round(component.incoming_qty, precision_rounding=rounding),
|
||||
"outgoing_qty": float_round(component.outgoing_qty, precision_rounding=rounding),
|
||||
"free_qty": float_round(component.free_qty, precision_rounding=rounding),
|
||||
"virtual_available": component.uom_id.round(component.virtual_available),
|
||||
"qty_available": component.uom_id.round(component.qty_available),
|
||||
"incoming_qty": component.uom_id.round(component.incoming_qty),
|
||||
"outgoing_qty": component.uom_id.round(component.outgoing_qty),
|
||||
"free_qty": component.uom_id.round(component.free_qty),
|
||||
}
|
||||
)
|
||||
ratios_virtual_available.append(float_round(component_res["virtual_available"] / qty_per_kit, precision_rounding=rounding, rounding_method='DOWN'))
|
||||
ratios_qty_available.append(float_round(component_res["qty_available"] / qty_per_kit, precision_rounding=rounding, rounding_method='DOWN'))
|
||||
ratios_incoming_qty.append(float_round(component_res["incoming_qty"] / qty_per_kit, precision_rounding=rounding, rounding_method='DOWN'))
|
||||
ratios_outgoing_qty.append(float_round(component_res["outgoing_qty"] / qty_per_kit, precision_rounding=rounding, rounding_method='DOWN'))
|
||||
ratios_free_qty.append(float_round(component_res["free_qty"] / qty_per_kit, precision_rounding=rounding, rounding_method='DOWN'))
|
||||
ratios_virtual_available.append(component.uom_id.round(component_res["virtual_available"] / qty_per_kit, rounding_method='DOWN'))
|
||||
ratios_qty_available.append(component.uom_id.round(component_res["qty_available"] / qty_per_kit, rounding_method='DOWN'))
|
||||
ratios_incoming_qty.append(component.uom_id.round(component_res["incoming_qty"] / qty_per_kit, rounding_method='DOWN'))
|
||||
ratios_outgoing_qty.append(component.uom_id.round(component_res["outgoing_qty"] / qty_per_kit, rounding_method='DOWN'))
|
||||
ratios_free_qty.append(component.uom_id.round(component_res["free_qty"] / qty_per_kit, rounding_method='DOWN'))
|
||||
if bom_sub_lines and ratios_virtual_available: # Guard against all cnsumable bom: at least one ratio should be present.
|
||||
res[product.id] = {
|
||||
'virtual_available': float_round(min(ratios_virtual_available) * bom_kits[product].product_qty, precision_rounding=rounding) // 1,
|
||||
'qty_available': float_round(min(ratios_qty_available) * bom_kits[product].product_qty, precision_rounding=rounding) // 1,
|
||||
'incoming_qty': float_round(min(ratios_incoming_qty) * bom_kits[product].product_qty, precision_rounding=rounding) // 1,
|
||||
'outgoing_qty': float_round(min(ratios_outgoing_qty) * bom_kits[product].product_qty, precision_rounding=rounding) // 1,
|
||||
'free_qty': float_round(min(ratios_free_qty) * bom_kits[product].product_qty, precision_rounding=rounding) // 1,
|
||||
'virtual_available': component.uom_id.round(min(ratios_virtual_available) * bom_kits[product].product_qty) // 1,
|
||||
'qty_available': component.uom_id.round(min(ratios_qty_available) * bom_kits[product].product_qty) // 1,
|
||||
'incoming_qty': component.uom_id.round(min(ratios_incoming_qty) * bom_kits[product].product_qty) // 1,
|
||||
'outgoing_qty': component.uom_id.round(min(ratios_outgoing_qty) * bom_kits[product].product_qty) // 1,
|
||||
'free_qty': component.uom_id.round(min(ratios_free_qty) * bom_kits[product].product_qty) // 1,
|
||||
}
|
||||
else:
|
||||
res[product.id] = {
|
||||
|
|
@ -340,28 +372,9 @@ class ProductProduct(models.Model):
|
|||
components |= self.env['product.product'].concat(*[l[0].product_id for l in bom_sub_lines])
|
||||
res = super(ProductProduct, components).action_open_quants()
|
||||
if bom_kits:
|
||||
res['context']['single_product'] = False
|
||||
res['context'].pop('default_product_tmpl_id', None)
|
||||
return res
|
||||
|
||||
def action_compute_bom_days(self):
|
||||
bom_by_products = self.env['mrp.bom']._bom_find(self)
|
||||
company_id = self.env.context.get('default_company_id', self.env.company.id)
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1)
|
||||
for product in self:
|
||||
bom_data = self.env['report.mrp.report_bom_structure'].with_context(minimized=True)._get_bom_data(bom_by_products[product], warehouse, product, ignore_stock=True)
|
||||
if bom_data.get('availability_state') == 'unavailable' and not bom_data.get('components_available', True):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Cannot compute days to prepare due to missing route info for at least 1 component or for the final product.'),
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
availability_delay = bom_data.get('resupply_avail_delay')
|
||||
product.days_to_prepare_mo = availability_delay - bom_data.get('lead_time', 0) if availability_delay else 0
|
||||
|
||||
def _match_all_variant_values(self, product_template_attribute_value_ids):
|
||||
""" It currently checks that all variant values (`product_template_attribute_value_ids`)
|
||||
are in the product (`self`).
|
||||
|
|
@ -376,19 +389,19 @@ class ProductProduct(models.Model):
|
|||
# * the attributes are a subset of the attributes of the line.
|
||||
return len(self.product_template_attribute_value_ids & product_template_attribute_value_ids) == len(product_template_attribute_value_ids.attribute_id)
|
||||
|
||||
def _count_returned_sn_products(self, sn_lot):
|
||||
res = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', sn_lot.id),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done'),
|
||||
def _count_returned_sn_products_domain(self, sn_lot, or_domains):
|
||||
or_domains.append([
|
||||
('production_id', '=', False),
|
||||
('location_id.usage', '=', 'production'),
|
||||
('move_id.unbuild_id', '!=', False),
|
||||
])
|
||||
return super()._count_returned_sn_products(sn_lot) + res
|
||||
return super()._count_returned_sn_products_domain(sn_lot, or_domains)
|
||||
|
||||
def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
|
||||
'''extending the method in stock.product to take into account kits'''
|
||||
op = PY_OPERATORS.get(operator)
|
||||
if not op:
|
||||
return NotImplemented
|
||||
product_ids = super(ProductProduct, self)._search_qty_available_new(operator, value, lot_id, owner_id, package_id)
|
||||
kit_boms = self.env['mrp.bom'].search([('type', "=", 'phantom')])
|
||||
kit_products = self.env['product.product']
|
||||
|
|
@ -398,8 +411,10 @@ class ProductProduct(models.Model):
|
|||
else:
|
||||
kit_products |= kit.product_tmpl_id.product_variant_ids
|
||||
for product in kit_products:
|
||||
if OPERATORS[operator](product.qty_available, value):
|
||||
if op(product.qty_available, value):
|
||||
product_ids.append(product.id)
|
||||
elif product.id in product_ids:
|
||||
product_ids.pop(product_ids.index(product.id))
|
||||
return list(set(product_ids))
|
||||
|
||||
def action_archive(self):
|
||||
|
|
@ -418,3 +433,45 @@ class ProductProduct(models.Model):
|
|||
},
|
||||
}
|
||||
return res
|
||||
|
||||
def _get_backend_root_menu_ids(self):
|
||||
return super()._get_backend_root_menu_ids() + [self.env.ref('mrp.menu_mrp_root').id]
|
||||
|
||||
def _update_uom(self, to_uom_id):
|
||||
for uom, product_template, boms in self.env['mrp.bom']._read_group(
|
||||
[('product_tmpl_id', 'in', self.product_tmpl_id.ids)],
|
||||
['product_uom_id', 'product_tmpl_id'],
|
||||
['id:recordset'],
|
||||
):
|
||||
if product_template.uom_id != uom:
|
||||
raise UserError(_('As other units of measure (ex : %(problem_uom)s) '
|
||||
'than %(uom)s have already been used for this product, the change of unit of measure can not be done.'
|
||||
'If you want to change it, please archive the product and create a new one.',
|
||||
problem_uom=uom.name, uom=product_template.uom_id.name))
|
||||
boms.product_uom_id = to_uom_id
|
||||
|
||||
for uom, product, bom_lines in self.env['mrp.bom.line']._read_group(
|
||||
[('product_id', 'in', self.ids)],
|
||||
['product_uom_id', 'product_id'],
|
||||
['id:recordset'],
|
||||
):
|
||||
if product.product_tmpl_id.uom_id != uom:
|
||||
raise UserError(_('As other units of measure (ex : %(problem_uom)s) '
|
||||
'than %(uom)s have already been used for this product, the change of unit of measure can not be done.'
|
||||
'If you want to change it, please archive the product and create a new one.',
|
||||
problem_uom=uom.name, uom=product.product_tmpl_id.uom_id.name))
|
||||
bom_lines.product_uom_id = to_uom_id
|
||||
|
||||
for uom, product, productions in self.env['mrp.production']._read_group(
|
||||
[('product_id', 'in', self.ids)],
|
||||
['product_uom_id', 'product_id'],
|
||||
['id:recordset'],
|
||||
):
|
||||
if product.product_tmpl_id.uom_id != uom:
|
||||
raise UserError(_('As other units of measure (ex : %(problem_uom)s) '
|
||||
'than %(uom)s have already been used for this product, the change of unit of measure can not be done.'
|
||||
'If you want to change it, please archive the product and create a new one.',
|
||||
problem_uom=uom.name, uom=product.product_tmpl_id.uom_id.name))
|
||||
productions.product_uom_id = to_uom_id
|
||||
|
||||
return super()._update_uom(to_uom_id)
|
||||
|
|
|
|||
23
odoo-bringout-oca-ocb-mrp/mrp/models/product_document.py
Normal file
23
odoo-bringout-oca-ocb-mrp/mrp/models/product_document.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductDocument(models.Model):
|
||||
_inherit = 'product.document'
|
||||
|
||||
def _default_attached_on_mrp(self):
|
||||
return "bom" if self.env.context.get('attached_on_bom') else "hidden"
|
||||
|
||||
attached_on_mrp = fields.Selection(
|
||||
selection=[
|
||||
('hidden', "Hidden"),
|
||||
('bom', "Bill of Materials")
|
||||
],
|
||||
required=True,
|
||||
string="MRP : Visible at",
|
||||
help="Leave hidden if document only accessible on product form.\n"
|
||||
"Select Bill of Materials to visualise this document as a product attachment when this product is in a bill of material.",
|
||||
default=lambda self: self._default_attached_on_mrp(),
|
||||
)
|
||||
|
|
@ -4,13 +4,9 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
manufacturing_lead = fields.Float(
|
||||
'Manufacturing Lead Time', default=0.0, required=True,
|
||||
help="Security days for each manufacturing operation.")
|
||||
|
||||
def _create_unbuild_sequence(self):
|
||||
unbuild_vals = []
|
||||
for company in self:
|
||||
|
|
@ -34,10 +30,5 @@ class Company(models.Model):
|
|||
company_todo_sequence._create_unbuild_sequence()
|
||||
|
||||
def _create_per_company_sequences(self):
|
||||
super(Company, self)._create_per_company_sequences()
|
||||
super()._create_per_company_sequences()
|
||||
self._create_unbuild_sequence()
|
||||
|
||||
def _get_security_by_rule_action(self):
|
||||
res = super()._get_security_by_rule_action()
|
||||
res['manufacture'] = self.manufacturing_lead
|
||||
return res
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue