feat: Migrate to maintenance.equipment with automated OpenUpgrade workflow
- Integrated maintenance.equipment using _inherits pattern in open_workshop_base
- Removed duplicate fields (code, description, storage_location, purchase_price, purchase_date)
- Delegated equipment management to OCA maintenance module
- Added Smart Button UI pattern for equipment details
- Implemented automated migration workflow:
* SQL script renames module open_workshop → open_workshop_base
* Pre-migration: Installs maintenance, adds equipment_id column
* Post-migration: Migrates 23 machines with proper JSONB names and locations
- Restored old open_workshop module (installable=False) for DB compatibility
- Updated CI/CD workflow with migration steps
- Area mapping corrected: area.name → equipment.location
- JSONB handling: Using SQL jsonb_build_object() for proper storage
- Serial numbers: From old code field or generated as OWS-{id}
Tested and verified:
✅ 23 machines successfully migrated
✅ JSONB names extractable: name->>'de_DE' and name->>'en_US'
✅ Locations correctly mapped: Fablab, Holzbereich, etc.
✅ equipment_id linkage functional
This commit is contained in:
parent
ceb8af7e48
commit
8fb58c744e
3
open_workshop/__init__.py
Normal file
3
open_workshop/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
||||
39
open_workshop/__manifest__.py
Normal file
39
open_workshop/__manifest__.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
'name': 'POS Open Workshop',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.1',
|
||||
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
|
||||
'depends': ['base', 'account', 'hr','product','sale','contacts','point_of_sale'],
|
||||
'author': 'matthias.lotz',
|
||||
'category': 'Point of Sale',
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/machine_product_training_views.xml',
|
||||
'views/menu_views.xml',
|
||||
'views/machine_area_views.xml',
|
||||
'views/machine_views.xml',
|
||||
'views/res_partner_view.xml',
|
||||
'data/data.xml',
|
||||
],
|
||||
'installable': False, # Wird durch open_workshop_base ersetzt
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'open_workshop/static/src/css/category_color.css',
|
||||
],
|
||||
'point_of_sale._assets_pos': [
|
||||
'open_workshop/static/src/css/pos.css',
|
||||
'open_workshop/static/src/js/ows_machine_access_list.js',
|
||||
'open_workshop/static/src/js/ows_pos_customer_sidebar.js',
|
||||
'open_workshop/static/src/js/ows_pos_sidebar.js',
|
||||
'open_workshop/static/src/js/ows_product_screen_template_patch.js',
|
||||
'open_workshop/static/src/xml/ows_machine_access_list.xml',
|
||||
'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml',
|
||||
'open_workshop/static/src/xml/ows_pos_sidebar.xml',
|
||||
'open_workshop/static/src/xml/ows_product_screen_template_patch.xml',
|
||||
],
|
||||
},
|
||||
'description': """
|
||||
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
|
||||
Die App ist für den Einsatz in der Odoo-Version 18.0 konzipiert.
|
||||
""",
|
||||
}
|
||||
3
open_workshop/controllers/__init__.py
Normal file
3
open_workshop/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Datei: open_workshop/controllers/__init__.py
|
||||
|
||||
from . import pos_access
|
||||
14
open_workshop/controllers/pos_access.py
Normal file
14
open_workshop/controllers/pos_access.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class OpenWorkshopPOSController(http.Controller):
|
||||
|
||||
@http.route('/open_workshop/partner_access', type='json', auth='user')
|
||||
def get_partner_machine_access(self, **kwargs):
|
||||
partner_id = kwargs.get('params', {}).get('partner_id')
|
||||
if not partner_id:
|
||||
return {"error": "Missing partner_id"}
|
||||
|
||||
Machine = request.env['ows.machine'].sudo()
|
||||
return Machine.get_access_list_grouped(partner_id)
|
||||
|
||||
153
open_workshop/data/data.xml
Normal file
153
open_workshop/data/data.xml
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<odoo>
|
||||
<!-- Bereiche -->
|
||||
<record id="area_fablab" model="ows.machine.area">
|
||||
<field name="name">Fablab</field>
|
||||
<field name="color_hex">#008000</field>
|
||||
</record>
|
||||
|
||||
<record id="area_holz" model="ows.machine.area">
|
||||
<field name="name">Holzbereich</field>
|
||||
<field name="color_hex">#ff0000</field>
|
||||
</record>
|
||||
|
||||
<record id="area_metall" model="ows.machine.area">
|
||||
<field name="name">Metallbereich</field>
|
||||
<field name="color_hex">#0000ff</field>
|
||||
</record>
|
||||
|
||||
<record id="area_elektronik" model="ows.machine.area">
|
||||
<field name="name">Elektronikbereich</field>
|
||||
<field name="color_hex">#ffff00</field>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Fablab -->
|
||||
<record id="machine_sabako_laser" model="ows.machine">
|
||||
<field name="name">Sabako Laser</field>
|
||||
<field name="code">sabako_laser</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa" model="ows.machine">
|
||||
<field name="name">Prusa</field>
|
||||
<field name="code">prusa</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa_mmu" model="ows.machine">
|
||||
<field name="name">Prusa MMU</field>
|
||||
<field name="code">prusa_mmu</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_3d_delta" model="ows.machine">
|
||||
<field name="name">3D Delta</field>
|
||||
<field name="code">3d_delta</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_cnc_beamicon" model="ows.machine">
|
||||
<field name="name">CNC Beamicon</field>
|
||||
<field name="code">cnc_beamicon</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Holzbereich -->
|
||||
<record id="machine_formatkreissaege" model="ows.machine">
|
||||
<field name="name">Formatkreissäge</field>
|
||||
<field name="code">formatkreissaege</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_holz" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_holz</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abrichte" model="ows.machine">
|
||||
<field name="name">Abricht Dickenhobel</field>
|
||||
<field name="code">dickenhobel</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drechselbank" model="ows.machine">
|
||||
<field name="name">Drechselbank</field>
|
||||
<field name="code">drechselbank</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_festool_domino" model="ows.machine">
|
||||
<field name="name">Festool Domino Fräse</field>
|
||||
<field name="code">festool_domino</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_maffel_duo" model="ows.machine">
|
||||
<field name="name">Maffel Duo Dübler</field>
|
||||
<field name="code">maffel_duo</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_lamello" model="ows.machine">
|
||||
<field name="name">Lamello Zeta P2</field>
|
||||
<field name="code">lamello_zeta_p2</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Metallbereich -->
|
||||
<record id="machine_kreissaege_metall" model="ows.machine">
|
||||
<field name="name">Kreissäge</field>
|
||||
<field name="code">kreissaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_metall" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_mig_mag" model="ows.machine">
|
||||
<field name="name">MIG/MAG Schweißgeräte</field>
|
||||
<field name="code">mig_mag</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_wig" model="ows.machine">
|
||||
<field name="name">WIG Schweißgerät</field>
|
||||
<field name="code">wig</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_schweissen" model="ows.machine">
|
||||
<field name="name">Schweißen allgemein</field>
|
||||
<field name="code">schweissen_allgemein</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drehbank" model="ows.machine">
|
||||
<field name="name">Drehbank</field>
|
||||
<field name="code">drehbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_fraese" model="ows.machine">
|
||||
<field name="name">Fräse</field>
|
||||
<field name="code">fraese</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abkantbank" model="ows.machine">
|
||||
<field name="name">Abkantbank</field>
|
||||
<field name="code">abkantbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschine im Elektronikbereich -->
|
||||
<record id="machine_loetkolben" model="ows.machine">
|
||||
<field name="name">Lötkolben</field>
|
||||
<field name="code">loetkolben</field>
|
||||
<field name="area_id" ref="area_elektronik"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
5
open_workshop/models/__init__.py
Normal file
5
open_workshop/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import ows_models
|
||||
from . import pos_order
|
||||
|
||||
|
||||
|
||||
589
open_workshop/models/ows_models.py
Normal file
589
open_workshop/models/ows_models.py
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# ows_models.py
|
||||
# Part of Odoo Open Workshop
|
||||
|
||||
from odoo import models, fields, api
|
||||
from markupsafe import escape as html_escape
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ ows_models.py geladen")
|
||||
|
||||
class HREmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.model
|
||||
def anonymize_for_testsystem(self):
|
||||
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
|
||||
admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1)
|
||||
if not admin_user:
|
||||
_logger.error("[OWS] Administrator-Benutzer nicht gefunden!")
|
||||
return
|
||||
|
||||
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
|
||||
|
||||
# Suche auch archivierte Employees
|
||||
admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1)
|
||||
|
||||
if admin_employee:
|
||||
# Administrator-Employee reaktivieren und umbenennen
|
||||
admin_employee.write({
|
||||
'name': 'TESTSYSTEM',
|
||||
'job_title': 'Testumgebung',
|
||||
'work_email': 'office@hobbyhimmel.de',
|
||||
'work_phone': False,
|
||||
'active': True, # Reaktivieren falls archiviert
|
||||
})
|
||||
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
|
||||
else:
|
||||
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
|
||||
return
|
||||
|
||||
# Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen)
|
||||
other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)])
|
||||
other_employees.write({'active': False})
|
||||
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_logger.info("✅ ows ResPartner geladen")
|
||||
|
||||
ows_user_id = fields.One2many('ows.user', 'partner_id', string="OWS Benutzerdaten")
|
||||
|
||||
# Alte Felder (weiterhin sichtbar für externe Programme)
|
||||
birthday = fields.Date(
|
||||
string="Geburtstag",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_birthday',
|
||||
store=False
|
||||
)
|
||||
rfid_card = fields.Text(
|
||||
string="RFID Card ID",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_rfid_card',
|
||||
store=False
|
||||
)
|
||||
security_briefing = fields.Boolean(
|
||||
string="Haftungsausschluss",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_security_briefing',
|
||||
store=False
|
||||
)
|
||||
security_id = fields.Text(
|
||||
string="Haftungsausschluss ID",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_security_id',
|
||||
store=False
|
||||
)
|
||||
# Neue direkte vvow_* Felder
|
||||
vvow_birthday = fields.Date(string="Geburtstag (vvow)")
|
||||
vvow_rfid_card = fields.Text(string="RFID Card ID (vvow)")
|
||||
vvow_security_briefing = fields.Boolean(string="Haftungsausschluss (vvow)")
|
||||
vvow_security_id = fields.Text(string="Haftungsausschluss ID (vvow)")
|
||||
|
||||
@api.depends('ows_user_id')
|
||||
def _compute_ows_user_fields(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
partner.birthday = user.birthday if user else False
|
||||
partner.rfid_card = user.rfid_card if user else False
|
||||
partner.security_briefing = user.security_briefing if user else False
|
||||
partner.security_id = user.security_id if user else False
|
||||
|
||||
def _inverse_birthday(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.birthday = partner.birthday
|
||||
|
||||
def _inverse_rfid_card(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.rfid_card = partner.rfid_card
|
||||
|
||||
def _inverse_security_briefing(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.security_briefing = partner.security_briefing
|
||||
|
||||
def _inverse_security_id(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.security_id = partner.security_id
|
||||
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
partners = super().create(vals_list)
|
||||
for vals, partner in zip(vals_list, partners):
|
||||
self.env['ows.user'].create({
|
||||
'partner_id': partner.id,
|
||||
'birthday': vals.get('birthday') or vals.get('vvow_birthday'),
|
||||
'rfid_card': vals.get('rfid_card') or vals.get('vvow_rfid_card'),
|
||||
'security_briefing': vals.get('security_briefing') or vals.get('vvow_security_briefing'),
|
||||
'security_id': vals.get('security_id') or vals.get('vvow_security_id'),
|
||||
})
|
||||
return partners
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if not user:
|
||||
continue
|
||||
|
||||
# Synchronisation alt -> user
|
||||
if 'birthday' in vals:
|
||||
user.birthday = vals['birthday']
|
||||
if 'rfid_card' in vals:
|
||||
user.rfid_card = vals['rfid_card']
|
||||
if 'security_briefing' in vals:
|
||||
user.security_briefing = vals['security_briefing']
|
||||
if 'security_id' in vals:
|
||||
user.security_id = vals['security_id']
|
||||
|
||||
# Synchronisation vvow_* -> user + alt
|
||||
if 'vvow_birthday' in vals:
|
||||
user.birthday = vals['vvow_birthday']
|
||||
partner.birthday = vals['vvow_birthday']
|
||||
if 'vvow_rfid_card' in vals:
|
||||
user.rfid_card = vals['vvow_rfid_card']
|
||||
partner.rfid_card = vals['vvow_rfid_card']
|
||||
if 'vvow_security_briefing' in vals:
|
||||
user.security_briefing = vals['vvow_security_briefing']
|
||||
partner.security_briefing = vals['vvow_security_briefing']
|
||||
if 'vvow_security_id' in vals:
|
||||
user.security_id = vals['vvow_security_id']
|
||||
partner.security_id = vals['vvow_security_id']
|
||||
return res
|
||||
|
||||
machine_access_ids = fields.One2many(
|
||||
'ows.machine.access',
|
||||
'partner_id',
|
||||
string='Maschinenfreigaben'
|
||||
)
|
||||
|
||||
machine_access_html = fields.Html(
|
||||
string="Maschinenfreigabe",
|
||||
compute="_compute_machine_access_html",
|
||||
sanitize=False
|
||||
)
|
||||
|
||||
|
||||
@api.depends('machine_access_ids')
|
||||
def _compute_machine_access_html(self):
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
for partner in self:
|
||||
html = ""
|
||||
for area in areas:
|
||||
html += f"""
|
||||
<div class="o_form_sheet">
|
||||
<h3 class="o_form_label">{area.name}</h3>
|
||||
<table class="table table-sm table-bordered o_form_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Maschine</th>
|
||||
<th>Status</th>
|
||||
<th>Datum</th>
|
||||
<th>Gültig bis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
|
||||
|
||||
for machine in machines:
|
||||
access = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine.id),
|
||||
], limit=1)
|
||||
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
|
||||
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
|
||||
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{machine.name}</td>
|
||||
<td>{icon}</td>
|
||||
<td>{date_granted}</td>
|
||||
<td>{date_expiry}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += "</tbody></table></div>"
|
||||
|
||||
partner.machine_access_html = html
|
||||
|
||||
|
||||
|
||||
|
||||
@api.model
|
||||
def migrate_existing_partners(self):
|
||||
"""
|
||||
Erstellt für alle vorhandenen res.partner einen ows.user,
|
||||
wenn noch keiner existiert, und übernimmt alte vvow_* Felder.
|
||||
Führt am Ende automatisch einen commit durch.
|
||||
|
||||
Verwendung in der odoo shell:
|
||||
env['res.partner'].migrate_existing_partners()
|
||||
env['ows.user'].search_count([])
|
||||
env['ows.user'].search([]).mapped('partner_id.name')
|
||||
"""
|
||||
migrated = 0
|
||||
skipped = 0
|
||||
|
||||
partners = self.env['res.partner'].search([])
|
||||
for partner in partners:
|
||||
if partner.ows_user_id:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Werte lesen (werden evtl. durch _inherit hinzugefügt)
|
||||
vals = {
|
||||
'partner_id': partner.id,
|
||||
'birthday': getattr(partner, 'vvow_birthday', False),
|
||||
'rfid_card': getattr(partner, 'vvow_rfid_card', False),
|
||||
'security_briefing': getattr(partner, 'vvow_security_briefing', False),
|
||||
'security_id': getattr(partner, 'vvow_security_id', False),
|
||||
}
|
||||
self.env['ows.user'].create(vals)
|
||||
migrated += 1
|
||||
_logger.info(f"Erzeuge ows.user für Partner {partner.id} ({partner.name}) mit Werten: {vals}")
|
||||
|
||||
_logger.info(f"[OWS Migration] ✅ Migriert: {migrated}, ❌ Übersprungen: {skipped}")
|
||||
|
||||
# 🔐 Commit am Ende
|
||||
self.env.cr.commit()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': "Migration abgeschlossen",
|
||||
'message': f"{migrated} Partner migriert, {skipped} übersprungen.",
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def migrate_machine_access_from_old_fields(self):
|
||||
"""
|
||||
Migriert alte vvow_* Boolean-Felder in strukturierte ows.machine.access Einträge.
|
||||
Das Freigabe-Datum wird aus dem Änderungsverlauf (mail.message.tracking_value_ids) extrahiert.
|
||||
"""
|
||||
mapping = [
|
||||
('vvow_holz_sander', 'bandschleifer'),
|
||||
('vvow_holz_felder_bandsaw', 'bandsaege_holz'),
|
||||
('vvow_holz_felder_jointer', 'dickenhobel'),
|
||||
('vvow_holz_felder_mill', 'felder_fraese'),
|
||||
('vvow_holz_felder_tablesaw', 'formatkreissaege'),
|
||||
('vvow_holz_mill', 'tischfraese'),
|
||||
('vvow_holz_lathe', 'drechselbank'),
|
||||
('vvow_holz_domino', 'festool_domino'),
|
||||
('vvow_holz_duoduebler', 'maffel_duo'),
|
||||
('vvow_holz_lamello', 'lamello_zeta_p2'),
|
||||
('vvow_fablab_laser_sabko', 'sabako_laser'),
|
||||
('vvow_fablab_3dprint_delta', '3d_delta'),
|
||||
('vvow_fablab_3dprint_prusa', 'prusa'),
|
||||
('vvow_fablab_3dprint_prusa_mmu', 'prusa_mmu'),
|
||||
('vvow_fablab_cnc_beamicon', 'cnc_beamicon'),
|
||||
('vvow_metall_welding_mig', 'mig_mag'),
|
||||
('vvow_metall_welding_wig', 'wig'),
|
||||
('vvow_metall_welding_misc', 'schweissen_allgemein'),
|
||||
('vvow_metall_lathe', 'drehbank'),
|
||||
('vvow_metall_bandsaw', 'bandsaege_metall'),
|
||||
('vvow_metall_mill_fp2', 'fraese'),
|
||||
('vvow_metall_bende', 'abkantbank'),
|
||||
('vvow_metall_chop_saw', 'kreissaege_metall'),
|
||||
]
|
||||
|
||||
MachineAccess = self.env['ows.machine.access']
|
||||
MailMessage = self.env['mail.message']
|
||||
TrackingValue = self.env['mail.tracking.value']
|
||||
|
||||
count_created = 0
|
||||
|
||||
for partner in self.search([]):
|
||||
for field_name, machine_code in mapping:
|
||||
if not getattr(partner, field_name, False):
|
||||
continue
|
||||
|
||||
machine = self.env['ows.machine'].search([('code', '=', machine_code)], limit=1)
|
||||
if not machine:
|
||||
continue
|
||||
|
||||
# Änderungsverlauf durchsuchen: Wann wurde das Feld auf True gesetzt?
|
||||
tracking = TrackingValue.search([
|
||||
('field', '=', field_name),
|
||||
('field_type', '=', 'boolean'),
|
||||
('new_value_integer', '=', 1),
|
||||
('mail_message_id.model', '=', 'res.partner'),
|
||||
('mail_message_id.res_id', '=', partner.id),
|
||||
], order='id ASC', limit=1)
|
||||
|
||||
date_granted = tracking.mail_message_id.date if tracking else fields.Datetime.now()
|
||||
|
||||
if not MachineAccess.search([('partner_id', '=', partner.id), ('machine_id', '=', machine.id)]):
|
||||
MachineAccess.create({
|
||||
'partner_id': partner.id,
|
||||
'machine_id': machine.id,
|
||||
'date_granted': date_granted,
|
||||
})
|
||||
count_created += 1
|
||||
|
||||
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
|
||||
self.env.cr.commit()
|
||||
|
||||
@api.model
|
||||
def archive_partners_without_users(self):
|
||||
"""
|
||||
Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind.
|
||||
"""
|
||||
Partner = self.env['res.partner']
|
||||
User = self.env['res.users']
|
||||
|
||||
# IDs aller Partner, die ein Benutzerkonto haben
|
||||
user_partner_ids = User.search([]).mapped('partner_id').ids
|
||||
|
||||
# Alle Partner ohne Benutzerkonto
|
||||
partners_to_archive = Partner.search([
|
||||
('id', 'not in', user_partner_ids),
|
||||
('active', '=', True),
|
||||
])
|
||||
|
||||
count = len(partners_to_archive)
|
||||
partners_to_archive.write({'active': False})
|
||||
for p in partners_to_archive:
|
||||
_logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})")
|
||||
_logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.")
|
||||
self.env.cr.commit()
|
||||
|
||||
|
||||
class OwsUser(models.Model):
|
||||
_name = 'ows.user'
|
||||
_description = 'OWS: Benutzerdaten'
|
||||
_table = 'ows_user'
|
||||
_logger.info("✅ ows_user geladen")
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
string='Kontakt'
|
||||
)
|
||||
|
||||
birthday = fields.Date('Geburtstag')
|
||||
rfid_card = fields.Text('RFID Card ID')
|
||||
security_briefing = fields.Boolean('Haftungsausschluss', default=False)
|
||||
security_id = fields.Text('Haftungsausschluss ID')
|
||||
|
||||
_sql_constraints = [
|
||||
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
|
||||
]
|
||||
|
||||
|
||||
AVAILABLE_COLORS = [
|
||||
('#000000', 'schwarz'),
|
||||
('#ff0000', 'Rot'),
|
||||
('#E91E63', 'Pink'),
|
||||
('#9C27B0', 'Lila'),
|
||||
('#3F51B5', 'Indigo'),
|
||||
('#0000ff', 'Blau'),
|
||||
('#008000', 'Grün'),
|
||||
('#ffff00', 'Gelb'),
|
||||
('#FF9800', 'Orange'),
|
||||
('#795548', 'Braun'),
|
||||
('#ffffff', 'Weiss'),
|
||||
]
|
||||
|
||||
class OwsMachineArea(models.Model):
|
||||
_name = 'ows.machine.area'
|
||||
_table = 'ows_machine_area'
|
||||
_description = 'OWS: Maschinenbereich'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string="Name", required=True, translate=True)
|
||||
|
||||
color_hex = fields.Selection(
|
||||
selection=AVAILABLE_COLORS,
|
||||
string="Farbe (Hex)",
|
||||
required=True,
|
||||
)
|
||||
|
||||
color_hex_value = fields.Char(
|
||||
string="Farbcode",
|
||||
compute='_compute_color_hex_value',
|
||||
store=False
|
||||
)
|
||||
|
||||
color_name = fields.Char(
|
||||
string="Farbname",
|
||||
compute='_compute_color_name',
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_hex_value(self):
|
||||
for rec in self:
|
||||
rec.color_hex_value = rec.color_hex or ''
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_name(self):
|
||||
label_dict = dict(AVAILABLE_COLORS)
|
||||
for rec in self:
|
||||
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
|
||||
|
||||
|
||||
|
||||
class OwsMachine(models.Model):
|
||||
_name = 'ows.machine'
|
||||
_table = 'ows_machine'
|
||||
_description = 'OWS: Maschine'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
|
||||
category = fields.Selection([
|
||||
('green', 'Kategorie 1: grün'),
|
||||
('yellow', 'Kategorie 2: gelb'),
|
||||
('red', 'Kategorie 3: rot'),
|
||||
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
|
||||
"- grün: keine Einweisungspflicht\n"
|
||||
"- gelb: empfohlene Einweisung\n"
|
||||
"- rot: Einweisung zwingend erforderlich")
|
||||
|
||||
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
|
||||
|
||||
@api.depends('category')
|
||||
def _compute_category_icon(self):
|
||||
for rec in self:
|
||||
icon_map = {
|
||||
'green': '🟢',
|
||||
'yellow': '🟡',
|
||||
'red': '🔴',
|
||||
}
|
||||
rec.category_icon = icon_map.get(rec.category, '⚪')
|
||||
|
||||
description = fields.Text()
|
||||
active = fields.Boolean(default=True)
|
||||
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
|
||||
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
|
||||
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
|
||||
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
|
||||
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
|
||||
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
|
||||
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
|
||||
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
|
||||
|
||||
@api.depends('product_ids.product_id.name')
|
||||
def _compute_product_using_names(self):
|
||||
for machine in self:
|
||||
names = machine.product_ids.mapped('product_id.name')
|
||||
machine.product_names = ", ".join(names)
|
||||
|
||||
@api.depends('training_ids.training_id.name')
|
||||
def _compute_product_training_names(self):
|
||||
for machine in self:
|
||||
names = machine.training_ids.mapped('training_id.name')
|
||||
machine.training_names = ", ".join(names)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.')
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
return [(rec.id, f"{rec.name} ({rec.code})") for rec in self]
|
||||
|
||||
@api.model
|
||||
def get_access_list_grouped(self, partner_id):
|
||||
"""
|
||||
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
|
||||
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
|
||||
|
||||
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
|
||||
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
|
||||
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
|
||||
|
||||
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
|
||||
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
|
||||
|
||||
Args:
|
||||
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
|
||||
|
||||
Returns:
|
||||
dict: Ein Dictionary mit folgenden Schlüsseln:
|
||||
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
|
||||
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
|
||||
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
|
||||
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
|
||||
- 'birthday': Geburtstag des Nutzers (str oder '').
|
||||
"""
|
||||
partner = self.env['res.partner'].browse(partner_id)
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
_logger.info("Access RPC called with partner_id=%s", partner_id)
|
||||
access_by_area = []
|
||||
for area in areas:
|
||||
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
|
||||
machine_list = []
|
||||
for machine in machines:
|
||||
has_access = bool(self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('machine_id', '=', machine.id),
|
||||
], limit=1))
|
||||
machine_list.append({
|
||||
'name': machine.name,
|
||||
'has_access': has_access,
|
||||
})
|
||||
if machine_list:
|
||||
access_by_area.append({
|
||||
'area': area.name,
|
||||
'color_hex': area.color_hex or '#000000',
|
||||
'machines': machine_list
|
||||
})
|
||||
|
||||
user = partner.ows_user_id[:1]
|
||||
return {
|
||||
'access_by_area': access_by_area,
|
||||
'security_briefing': user.security_briefing if user else False,
|
||||
'security_id': user.security_id if user else '',
|
||||
'rfid_card': user.rfid_card if user else '',
|
||||
'birthday': user.birthday if user else '',
|
||||
}
|
||||
|
||||
|
||||
|
||||
class OwsMachineAccess(models.Model):
|
||||
_name = 'ows.machine.access'
|
||||
_table = 'ows_machine_access'
|
||||
_description = 'OWS: Maschinenfreigabe'
|
||||
_order = 'partner_id, machine_id'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True)
|
||||
date_granted = fields.Date(default=fields.Date.today)
|
||||
date_expiry = fields.Date(string="Ablaufdatum")
|
||||
granted_by_pos = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('partner_machine_unique', 'unique(partner_id, machine_id)', 'Der Kunde hat diese Freigabe bereits.')
|
||||
]
|
||||
|
||||
|
||||
class OwsMachineProduct(models.Model):
|
||||
_name = 'ows.machine.product'
|
||||
_table = 'ows_machine_product'
|
||||
_description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
|
||||
|
||||
product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||
|
||||
class OwsMachineTraining(models.Model):
|
||||
_name = 'ows.machine.training'
|
||||
_table = 'ows_machine_training'
|
||||
_description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
|
||||
|
||||
training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||
50
open_workshop/models/pos_order.py
Normal file
50
open_workshop/models/pos_order.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from odoo import models, fields, api
|
||||
from collections import defaultdict
|
||||
|
||||
#import debugpy
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ pos_order.py geladen Test 2")
|
||||
|
||||
#debugpy.listen(("0.0.0.0", 5678))
|
||||
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
|
||||
# Optional: Starte erst, wenn VS Code verbunden ist
|
||||
#debugpy.wait_for_client()
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
def _process_order(self, order, existing_order):
|
||||
_logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt'))
|
||||
pos_order_id = super(PosOrder, self)._process_order(order, existing_order)
|
||||
pos_order = self.browse(pos_order_id)
|
||||
|
||||
training_products = self.env['ows.machine.training'].search([])
|
||||
product_map = defaultdict(list)
|
||||
for tp in training_products:
|
||||
product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
|
||||
|
||||
partner = pos_order.partner_id
|
||||
if not partner:
|
||||
_logger.info("🟡 POS-Bestellung ohne Partner – keine Freigabe möglich")
|
||||
return pos_order_id
|
||||
|
||||
for line in pos_order.lines:
|
||||
product_tmpl_id = line.product_id.product_tmpl_id.id
|
||||
machine_ids = product_map.get(product_tmpl_id, [])
|
||||
_logger.info("🔍 Prüfe Produkt %s → Maschinen IDs: %s", line.product_id.display_name, machine_ids)
|
||||
for machine_id in machine_ids:
|
||||
already_exists = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine_id)
|
||||
], limit=1)
|
||||
if not already_exists:
|
||||
self.env['ows.machine.access'].create({
|
||||
'partner_id': partner.id,
|
||||
'machine_id': machine_id,
|
||||
'granted_by_pos': True
|
||||
})
|
||||
_logger.info("✅ Maschinenfreigabe erstellt: %s für %s", machine_id, partner.name)
|
||||
|
||||
return pos_order_id
|
||||
8
open_workshop/security/ir.model.access.csv
Normal file
8
open_workshop/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_ows_machine_access_user,ows.machine.access,model_ows_machine_access,base.group_user,1,1,1,1
|
||||
access_ows_machine_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1
|
||||
access_ows_machine_product_user,ows.machine.product,model_ows_machine_product,base.group_user,1,1,1,1
|
||||
access_ows_machine_training_user,access_ows_machine_training_user,model_ows_machine_training,base.group_user,1,1,1,1
|
||||
access_ows_machine_area,ows.machine.area,model_ows_machine_area,base.group_user,1,1,1,1
|
||||
access_ows_user,ows.user,model_ows_user,base.group_user,1,1,1,1
|
||||
access_ows_machine_training,ows.machine.training,model_ows_machine_training,base.group_user,1,1,1,1
|
||||
|
9
open_workshop/static/src/css/category_color.css
Normal file
9
open_workshop/static/src/css/category_color.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.category-color-circle {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
57
open_workshop/static/src/css/pos.css
Normal file
57
open_workshop/static/src/css/pos.css
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
.custompane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ows-sidebar {
|
||||
flex: 1 1 auto;
|
||||
width: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.order-entry:hover { cursor: pointer; }
|
||||
.order-entry.selected { background-color: #007bff; color: white; }
|
||||
|
||||
.ows-customer-list {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0; /* notwendig für Scrollbar */
|
||||
}
|
||||
|
||||
.client-details-grid {
|
||||
flex-shrink: 0;
|
||||
max-height: 60%;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pos *::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height:8px;
|
||||
}
|
||||
|
||||
.sidebar-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
.sidebar-date {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-name {
|
||||
flex-shrink: 1;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
72
open_workshop/static/src/js/ows_machine_access_list.js
Normal file
72
open_workshop/static/src/js/ows_machine_access_list.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// @odoo-module ows_machine_access_list.js
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class OwsMachineAccessList extends Component {
|
||||
static template = 'open_workshop.OwsMachineAccessList';
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
|
||||
this.state = useState({
|
||||
client: null,
|
||||
grouped_accesses: [],
|
||||
security_briefing: false,
|
||||
security_id: '',
|
||||
rfid_card: '',
|
||||
birthday: '',
|
||||
});
|
||||
|
||||
// 🔁 Reagiere auf Partnerwechsel über den Odoo-Bus
|
||||
useBus(this.env.bus, 'partner-changed', () => {
|
||||
this.updateAccessList();
|
||||
});
|
||||
|
||||
// 🔃 Beim Mounten initiale Daten laden
|
||||
this.updateAccessList();
|
||||
}
|
||||
|
||||
async updateAccessList() {
|
||||
const order = this.pos.get_order();
|
||||
const partner = order?.get_partner?.();
|
||||
this.state.client = partner || null;
|
||||
if (!partner) {
|
||||
this.state.grouped_accesses = [];
|
||||
this.state.security_briefing = false;
|
||||
this.state.security_id = '';
|
||||
this.state.rfid_card = '';
|
||||
this.state.birthday = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await rpc("/open_workshop/partner_access", {
|
||||
params: { partner_id: partner.id },
|
||||
});
|
||||
|
||||
this.state.grouped_accesses = data.access_by_area || [];
|
||||
this.state.security_briefing = data.security_briefing;
|
||||
this.state.security_id = data.security_id;
|
||||
this.state.rfid_card = data.rfid_card;
|
||||
this.state.birthday = data.birthday;
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Einweisungen:", error);
|
||||
this.state.grouped_accesses = [];
|
||||
this.state.security_briefing = false;
|
||||
this.state.security_id = '';
|
||||
this.state.rfid_card = '';
|
||||
this.state.birthday = '';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList);
|
||||
|
||||
54
open_workshop/static/src/js/ows_pos_customer_sidebar.js
Normal file
54
open_workshop/static/src/js/ows_pos_customer_sidebar.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// @odoo-module ows_pos_customer_sidebar.js
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ask } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
export class OwsPosCustomerSidebar extends Component {
|
||||
static template = "open_workshop.OwsPosCustomerSidebar";
|
||||
|
||||
setup() {
|
||||
this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store
|
||||
this.dialog = useService("dialog");
|
||||
}
|
||||
|
||||
addOrder() {
|
||||
this.pos.add_new_order(); // ✅ Neue Order wird aktive Order
|
||||
this.pos.showScreen("ProductScreen");
|
||||
this.pos.selectPartner();
|
||||
this.env.bus.trigger('partner-changed'); // ✅ Event manuell auslösen
|
||||
}
|
||||
|
||||
async removeCurrentOrder() {
|
||||
this.pos.onDeleteOrder(this.pos.get_order())
|
||||
}
|
||||
|
||||
openTicketScreen() {
|
||||
this.pos.showScreen("TicketScreen");
|
||||
}
|
||||
|
||||
// 🔧 FIXED: Zugriff auf Order-Liste korrigiert
|
||||
getFilteredOrderList() {
|
||||
return this.pos.get_open_orders();
|
||||
}
|
||||
|
||||
getDate(order) {
|
||||
const date = new Date(order.date_order);
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const hh = String(date.getHours()).padStart(2, '0');
|
||||
const mi = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${dd}.${mm}. ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
getPartner(order) {
|
||||
return order.get_partner()?.name || "Kein Kunde";
|
||||
}
|
||||
|
||||
selectOrder(order) {
|
||||
this.pos.set_order(order);
|
||||
this.env.bus.trigger('partner-changed');
|
||||
}
|
||||
}
|
||||
11
open_workshop/static/src/js/ows_pos_sidebar.js
Normal file
11
open_workshop/static/src/js/ows_pos_sidebar.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// ows_pos_sidebar.js
|
||||
// @odoo-module
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { OwsPosCustomerSidebar } from "./ows_pos_customer_sidebar";
|
||||
import { OwsMachineAccessList } from "./ows_machine_access_list";
|
||||
|
||||
export class OwsPosSidebar extends Component {
|
||||
static template = "open_workshop.OwsPosSidebar";
|
||||
static components = { OwsPosCustomerSidebar, OwsMachineAccessList };
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// product_screen_template_patch.js
|
||||
// @odoo-module
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { OwsPosSidebar } from "./ows_pos_sidebar";
|
||||
|
||||
class OwsProductScreen extends ProductScreen {
|
||||
static components = Object.assign({}, ProductScreen.components, {
|
||||
OwsPosSidebar,
|
||||
});
|
||||
}
|
||||
|
||||
registry.category("pos_screens").remove("ProductScreen");
|
||||
registry.category("pos_screens").add("ProductScreen", OwsProductScreen);
|
||||
76
open_workshop/static/src/xml/ows_machine_access_list.xml
Normal file
76
open_workshop/static/src/xml/ows_machine_access_list.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<t t-name="open_workshop.OwsMachineAccessList">
|
||||
<div class="client-details-grid p-2 small">
|
||||
|
||||
<!-- ✅ Sicherheitsbereich -->
|
||||
<t t-if="state.client">
|
||||
<div class="client-details-header">
|
||||
<ul>
|
||||
<li><span class="client-details-label">Einweisungen</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="client-details-area border" t-att-style="'border: solid 3px #ffffff; margin: 5px;'">
|
||||
<ul>
|
||||
<li class="client-detail">
|
||||
<span class="detail client-details-vvow_briefing">✅</span>
|
||||
<span class="briefinglabel">Werkstatt</span>
|
||||
</li>
|
||||
|
||||
<li class="client-detail">
|
||||
<t t-if="!state.security_briefing">
|
||||
<span class="detail client-details-vvow_briefing_error">❌</span>
|
||||
</t>
|
||||
<t t-if="state.security_briefing">
|
||||
<span class="detail client-details-vvow_briefing">✅</span>
|
||||
</t>
|
||||
<span class="briefinglabel">Haftungsausschluss</span>
|
||||
</li>
|
||||
|
||||
<t t-if="!state.security_briefing">
|
||||
<li class="client-detail">
|
||||
<ul class="subpoints">
|
||||
<span class="detail client-details-vvow_sec_briefing_error">‼️Bitte Prüfen‼️</span>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-if="state.security_briefing">
|
||||
<ul class="subpoints">
|
||||
<li class="client-detail">
|
||||
<span class="label">Id:</span>
|
||||
<span class="detail client-details-vvow_security_id">
|
||||
<t t-esc="state.security_id || 'N/A'" />
|
||||
</span>
|
||||
</li>
|
||||
<li class="client-detail">
|
||||
<span class="label">Geburtstag:</span>
|
||||
<span class="detail client-details-vvow_security_id">
|
||||
<t t-esc="state.birthday || 'N/A'" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ✅ Maschinenliste: immer sichtbar, gefiltert -->
|
||||
<t t-foreach="state.grouped_accesses" t-as="area" t-key="area.area">
|
||||
<t t-if="area.machines.length > 0">
|
||||
<div class="client-details-area" t-att-style="'border: solid 3px ' + area.color_hex + '; margin: 5px;'">
|
||||
<ul>
|
||||
<t t-foreach="area.machines" t-as="machine" t-key="machine.name">
|
||||
<li class="client-detail">
|
||||
<span t-attf-class="detail {{ machine.has_access ? 'client-details-vvow_briefing' : 'client-details-vvow_briefing_error' }}">
|
||||
<t t-esc="machine.has_access ? '✅' : '❌'" />
|
||||
</span>
|
||||
<span class="briefinglabel"><t t-esc="machine.name"/></span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
27
open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
Normal file
27
open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="open_workshop.OwsPosCustomerSidebar" owl="1">
|
||||
<div class="ows-sidebar p-2 bg-light border-end h-100">
|
||||
<div class="ows-sidebar-header mb-2 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-secondary" t-on-click="openTicketScreen">Orders</button>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-success me-1" t-on-click="addOrder">+</button>
|
||||
<button class="btn btn-sm btn-danger" t-on-click="removeCurrentOrder">–</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ows-customer-list overflow-auto">
|
||||
<t t-foreach="getFilteredOrderList()" t-as="order" t-key="order.uid">
|
||||
<!--div class="order-entry p-1 rounded mb-1 border"
|
||||
t-att-class="order === pos.get_order() ? 'bg-primary text-white' : 'bg-white'"
|
||||
t-on-click="() => selectOrder(order)"-->
|
||||
<div t-att-class="'order-entry' + (order === pos.get_order() ? ' selected' : '')" t-on-click="() => this.selectOrder(order)">
|
||||
<div class="sidebar-line">
|
||||
<span class="sidebar-date"><t t-esc="getDate(order)"/></span>
|
||||
<span class="sidebar-name" t-att-title="getPartner(order)"><t t-esc="getPartner(order)"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
9
open_workshop/static/src/xml/ows_pos_sidebar.xml
Normal file
9
open_workshop/static/src/xml/ows_pos_sidebar.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="open_workshop.OwsPosSidebar" owl="1">
|
||||
<div class="custompane d-flex flex-column border-end bg-200">
|
||||
<OwsPosCustomerSidebar />
|
||||
<OwsMachineAccessList />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="point_of_sale.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//div[contains(@class, 'leftpane')]" position="before">
|
||||
<OwsPosSidebar />
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
8
open_workshop/views/assets.xml
Normal file
8
open_workshop/views/assets.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<odoo>
|
||||
<template id="assets_open_workshop" inherit_id="point_of_sale._assets_pos">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/open_workshop/static/src/js/machine_access_sidebar.js"/>
|
||||
<link rel="stylesheet" type="text/css" href="/open_workshop/static/src/css/pos.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
40
open_workshop/views/machine_area_views.xml
Normal file
40
open_workshop/views/machine_area_views.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!-- machine_area_views.xml -->
|
||||
<odoo>
|
||||
<!-- Action zum Anzeigen der Bereiche -->
|
||||
<record id="action_machine_area_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinenbereiche</field>
|
||||
<field name="res_model">ows.machine.area</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüpunkt unter Maschinen > Konfiguration -->
|
||||
<menuitem id="menu_machine_area" name="Bereiche" parent="menu_machine_config" action="open_workshop.action_machine_area_list" sequence="30"/>
|
||||
|
||||
<!-- Listenansicht -->
|
||||
<record id="view_machine_area_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.area.tree</field>
|
||||
<field name="model">ows.machine.area</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="color_hex_value" string="Farbe (Hex)"/>
|
||||
<field name="color_name" string="Farbname"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Formularansicht -->
|
||||
<record id="view_machine_area_form" model="ir.ui.view">
|
||||
<field name="name">ows.machine.area.form</field>
|
||||
<field name="model">ows.machine.area</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Maschinenbereich">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="color_hex"/>
|
||||
<field name="color_name" readonly="1"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
49
open_workshop/views/machine_product_training_views.xml
Normal file
49
open_workshop/views/machine_product_training_views.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<odoo>
|
||||
<!-- Tree View: Nutzungsprodukte -->
|
||||
<record id="view_machine_product_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.product.tree</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id"/>
|
||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View: Einweisungsprodukte -->
|
||||
<record id="view_machine_training_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.training.tree</field>
|
||||
<field name="model">ows.machine.training</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id"/>
|
||||
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action: Nutzungsprodukte -->
|
||||
<record id="action_machine_product" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen-Nutzungsprodukte</field>
|
||||
<field name="res_model">ows.machine.product</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="view_id" ref="view_machine_product_tree"/>
|
||||
<field name="help" type="html">
|
||||
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action: Einweisungsprodukte -->
|
||||
<record id="action_machine_training" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen-Einweisungsprodukte</field>
|
||||
<field name="res_model">ows.machine.training</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="view_id" ref="view_machine_training_tree"/>
|
||||
<field name="help" type="html">
|
||||
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
68
open_workshop/views/machine_views.xml
Normal file
68
open_workshop/views/machine_views.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!-- machine_views.xml -->
|
||||
<odoo>
|
||||
<!-- Maschinen Listenansicht -->
|
||||
<record id="view_machine_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.tree</field>
|
||||
<field name="model">ows.machine</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id" widget="many2one_color"/>
|
||||
<field name="product_names"/>
|
||||
<field name="training_names"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen Formularansicht -->
|
||||
<record id="view_machine_form" model="ir.ui.view">
|
||||
<field name="name">ows.machine.form</field>
|
||||
<field name="model">ows.machine</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Maschine">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
|
||||
<!-- Notebook für Produkte und Einweisungen -->
|
||||
<notebook>
|
||||
<page string="Nutzungsprodukte">
|
||||
<field name="product_ids" context="{'default_machine_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]" />
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Einweisungsprodukte">
|
||||
<field name="training_ids" context="{'default_machine_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]" />
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
81
open_workshop/views/menu_views.xml
Normal file
81
open_workshop/views/menu_views.xml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!-- menu_views.xml -->
|
||||
<odoo>
|
||||
<!-- Maschinenliste -->
|
||||
<record id="action_machine_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen</field>
|
||||
<field name="res_model">ows.machine</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Trainingsprodukt-Liste -->
|
||||
<record id="action_training_product_list" model="ir.actions.act_window">
|
||||
<field name="name">Einweisungs-Produkte</field>
|
||||
<field name="res_model">ows.machine.product</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüstruktur -->
|
||||
<!-- Oberstes Menü -->
|
||||
<menuitem id="menu_machine_root"
|
||||
name="Maschinen"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Konfigurationsebene -->
|
||||
<menuitem id="menu_machine_config"
|
||||
name="Konfiguration"
|
||||
parent="menu_machine_root"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menüpunkt: Maschinenliste (klickbar) -->
|
||||
<menuitem id="menu_machine_list_action"
|
||||
name="Alle Maschinen"
|
||||
parent="menu_machine_config"
|
||||
action="open_workshop.action_machine_list"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menücontainer: Zuordnungen -->
|
||||
<menuitem id="menu_machine_list"
|
||||
name="Zuordnungen"
|
||||
parent="menu_machine_config"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Untermenü: Nutzungsprodukte -->
|
||||
<menuitem id="menu_machine_product"
|
||||
name="Nutzungsprodukte"
|
||||
parent="menu_machine_list"
|
||||
action="action_machine_product"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Untermenü: Einweisungsprodukte -->
|
||||
<menuitem id="menu_machine_training"
|
||||
name="Einweisungsprodukte"
|
||||
parent="menu_machine_list"
|
||||
action="action_machine_training"
|
||||
sequence="20"/>
|
||||
|
||||
|
||||
<!-- List & Form Views für training.product -->
|
||||
<record id="view_training_product_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.product.tree</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="product_id"/>
|
||||
<field name="machine_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_training_product_form" model="ir.ui.view">
|
||||
<field name="name">ows.machine.product.form</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Einweisungs-Produkt">
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<field name="machine_id"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
127
open_workshop/views/res_partner_view.xml
Normal file
127
open_workshop/views/res_partner_view.xml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<odoo>
|
||||
<!-- Entfernt die Partner-Warnung (Duplicate Bank Accounts) in res.partner
|
||||
<record id="patch_res_partner_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.remove.duplicate.bank.warning</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="account.view_partner_property_form"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='warning_tax' and @class='alert alert-warning oe_edit_only']" position="replace"/>
|
||||
</field>
|
||||
</record>-->
|
||||
|
||||
<!-- Entfernt die Bankkonto-Warnung in res.partner.bank
|
||||
<record id="patch_res_partner_bank_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.remove.duplicate.warning</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
|
||||
</field>
|
||||
</record> -->
|
||||
|
||||
<!-- Zentrale View für alle drei Tabs in garantierter Reihenfolge -->
|
||||
|
||||
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.ows.tabs</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
|
||||
<!-- EINWEISUNG: Zwei Felder nebeneinander -->
|
||||
<group name="container_row_2" string="Sicherheitseinweisung" col="2">
|
||||
<field name="security_briefing"/>
|
||||
<field name="security_id"/>
|
||||
</group>
|
||||
|
||||
<!-- MASCHINENFREIGABEN: Volle Breite -->
|
||||
<group string="Maschinenfreigaben" col="2">
|
||||
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
|
||||
<list>
|
||||
<field name="machine_id"/>
|
||||
<field name="date_granted"/>
|
||||
<field name="date_expiry"/>
|
||||
<field name="granted_by_pos"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
<!-- ÜBERSICHT: Volle Breite -->
|
||||
<group string="Maschinenfreigaben Übersicht" >
|
||||
<field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Geburtstag direkt nach der USt-ID -->
|
||||
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.ows.birthday</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority" eval="15"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="after">
|
||||
<field name="birthday"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View Anpassung -->
|
||||
<record id="ows_userList_inherit" model="ir.ui.view">
|
||||
<field name="name">res.partner.ows.tree</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="after">
|
||||
<field name="create_date" optional="show"/>
|
||||
<field name="security_briefing" optional="show"/>
|
||||
<field name="security_id" optional="show"/>
|
||||
<field name="rfid_card" optional="show"/>
|
||||
<field name="category_id" widget="many2many_tags"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='vat']" position="replace">
|
||||
<field name="vat" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='email']" position="replace">
|
||||
<field name="email" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='phone']" position="replace">
|
||||
<field name="phone" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='state_id']" position="replace">
|
||||
<field name="state_id" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='country_id']" position="replace">
|
||||
<field name="country_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Standardwerte setzen (company_type = person) -->
|
||||
<record id="view_partner_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.default_person</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_type" position="attributes">
|
||||
<attribute name="default">person</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Optional: Kontakte-Action, falls gebraucht -->
|
||||
<record id="contacts.action_contacts" model="ir.actions.act_window">
|
||||
<field name="view_mode">list,kanban,form,activity</field>
|
||||
</record>
|
||||
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
</record>
|
||||
<record id="contacts.action_contacts_view_tree" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
'name': 'Open Workshop Base',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.4',
|
||||
'summary': 'Kern-Modul für Maschinenfreigaben und Einweisungslogik',
|
||||
'depends': ['base', 'account', 'hr','product','sale','contacts'],
|
||||
'version': '18.0.1.0.4', # Migration läuft bei 18.0.1.0.4
|
||||
'summary': 'Kern-Modul für Maschinenfreigaben - vereinfachte Equipment-Integration',
|
||||
'depends': ['base', 'account', 'hr', 'product', 'sale', 'contacts', 'maintenance'],
|
||||
'author': 'matthias.lotz',
|
||||
'category': 'Manufacturing',
|
||||
|
||||
|
|
@ -28,11 +28,12 @@ Open Workshop Base - Kernmodul
|
|||
|
||||
Dieses Modul stellt die Basis-Funktionalität für Open Workshop bereit:
|
||||
|
||||
* Maschinenmodelle (ows.machine)
|
||||
* Maschinenmodelle (ows.machine) → nutzt maintenance.equipment als Single Source of Truth
|
||||
* Bereiche (ows.machine.area)
|
||||
* Einweisungslogik und Freigaben
|
||||
* Produktverknüpfungen für Einweisungen
|
||||
* Backend-UI (Form, Tree, Kanban)
|
||||
* Automatische Migration bestehender Maschinen zu maintenance.equipment
|
||||
|
||||
Für POS-Integration installiere: open_workshop_pos
|
||||
Für API-Zugriff installiere: open_workshop_api (erforderlich für WordPress Frontend)
|
||||
|
|
|
|||
|
|
@ -23,130 +23,130 @@
|
|||
<!-- Maschinen im Fablab -->
|
||||
<record id="machine_sabako_laser" model="ows.machine">
|
||||
<field name="name">Sabako Laser</field>
|
||||
<field name="code">sabako_laser</field>
|
||||
<field name="serial_no">sabako_laser</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa" model="ows.machine">
|
||||
<field name="name">Prusa</field>
|
||||
<field name="code">prusa</field>
|
||||
<field name="serial_no">prusa</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa_mmu" model="ows.machine">
|
||||
<field name="name">Prusa MMU</field>
|
||||
<field name="code">prusa_mmu</field>
|
||||
<field name="serial_no">prusa_mmu</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_3d_delta" model="ows.machine">
|
||||
<field name="name">3D Delta</field>
|
||||
<field name="code">3d_delta</field>
|
||||
<field name="serial_no">3d_delta</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_cnc_beamicon" model="ows.machine">
|
||||
<field name="name">CNC Beamicon</field>
|
||||
<field name="code">cnc_beamicon</field>
|
||||
<field name="serial_no">cnc_beamicon</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Holzbereich -->
|
||||
<record id="machine_formatkreissaege" model="ows.machine">
|
||||
<field name="name">Formatkreissäge</field>
|
||||
<field name="code">formatkreissaege</field>
|
||||
<field name="serial_no">formatkreissaege</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_holz" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_holz</field>
|
||||
<field name="serial_no">bandsaege_holz</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abrichte" model="ows.machine">
|
||||
<field name="name">Abricht Dickenhobel</field>
|
||||
<field name="code">dickenhobel</field>
|
||||
<field name="serial_no">dickenhobel</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drechselbank" model="ows.machine">
|
||||
<field name="name">Drechselbank</field>
|
||||
<field name="code">drechselbank</field>
|
||||
<field name="serial_no">drechselbank</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_festool_domino" model="ows.machine">
|
||||
<field name="name">Festool Domino Fräse</field>
|
||||
<field name="code">festool_domino</field>
|
||||
<field name="serial_no">festool_domino</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_maffel_duo" model="ows.machine">
|
||||
<field name="name">Maffel Duo Dübler</field>
|
||||
<field name="code">maffel_duo</field>
|
||||
<field name="serial_no">maffel_duo</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_lamello" model="ows.machine">
|
||||
<field name="name">Lamello Zeta P2</field>
|
||||
<field name="code">lamello_zeta_p2</field>
|
||||
<field name="serial_no">lamello_zeta_p2</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Metallbereich -->
|
||||
<record id="machine_kreissaege_metall" model="ows.machine">
|
||||
<field name="name">Kreissäge</field>
|
||||
<field name="code">kreissaege_metall</field>
|
||||
<field name="serial_no">kreissaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_metall" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_metall</field>
|
||||
<field name="serial_no">bandsaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_mig_mag" model="ows.machine">
|
||||
<field name="name">MIG/MAG Schweißgeräte</field>
|
||||
<field name="code">mig_mag</field>
|
||||
<field name="serial_no">mig_mag</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_wig" model="ows.machine">
|
||||
<field name="name">WIG Schweißgerät</field>
|
||||
<field name="code">wig</field>
|
||||
<field name="serial_no">wig</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_schweissen" model="ows.machine">
|
||||
<field name="name">Schweißen allgemein</field>
|
||||
<field name="code">schweissen_allgemein</field>
|
||||
<field name="serial_no">schweissen_allgemein</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drehbank" model="ows.machine">
|
||||
<field name="name">Drehbank</field>
|
||||
<field name="code">drehbank</field>
|
||||
<field name="serial_no">drehbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_fraese" model="ows.machine">
|
||||
<field name="name">Fräse</field>
|
||||
<field name="code">fraese</field>
|
||||
<field name="serial_no">fraese</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abkantbank" model="ows.machine">
|
||||
<field name="name">Abkantbank</field>
|
||||
<field name="code">abkantbank</field>
|
||||
<field name="serial_no">abkantbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschine im Elektronikbereich -->
|
||||
<record id="machine_loetkolben" model="ows.machine">
|
||||
<field name="name">Lötkolben</field>
|
||||
<field name="code">loetkolben</field>
|
||||
<field name="serial_no">loetkolben</field>
|
||||
<field name="area_id" ref="area_elektronik"/>
|
||||
</record>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
# Migration: open_workshop → open_workshop_base
|
||||
# Dieses Skript wird automatisch vor dem Modul-Update ausgeführt
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Benennt das Modul 'open_workshop' in 'open_workshop_base' um.
|
||||
|
||||
Wichtig:
|
||||
- Modellnamen (_name) bleiben unverändert (z.B. 'ows.machine')
|
||||
- Tabellennamen bleiben unverändert
|
||||
- Keine Datenmigration erforderlich
|
||||
- Nur Modul-Metadaten werden aktualisiert
|
||||
|
||||
Dieses Skript läuft VOR dem normalen Update-Prozess.
|
||||
"""
|
||||
|
||||
# Prüfen ob altes Modul existiert
|
||||
cr.execute("""
|
||||
SELECT id, state FROM ir_module_module
|
||||
WHERE name = 'open_workshop'
|
||||
""")
|
||||
old_module = cr.fetchone()
|
||||
|
||||
if old_module:
|
||||
_logger.info("=" * 70)
|
||||
_logger.info("MIGRATION: Renaming module 'open_workshop' to 'open_workshop_base'")
|
||||
_logger.info("=" * 70)
|
||||
|
||||
# 1. Modulnamen in ir_module_module aktualisieren
|
||||
cr.execute("""
|
||||
UPDATE ir_module_module
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop'
|
||||
""")
|
||||
_logger.info(f"✓ Updated ir_module_module: {cr.rowcount} row(s)")
|
||||
|
||||
# 2. Alle ir_model_data Referenzen aktualisieren
|
||||
cr.execute("""
|
||||
UPDATE ir_model_data
|
||||
SET module = 'open_workshop_base'
|
||||
WHERE module = 'open_workshop'
|
||||
""")
|
||||
_logger.info(f"✓ Updated ir_model_data: {cr.rowcount} row(s)")
|
||||
|
||||
# 3. Abhängigkeiten in anderen Modulen aktualisieren (falls vorhanden)
|
||||
cr.execute("""
|
||||
UPDATE ir_module_module_dependency
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop'
|
||||
""")
|
||||
if cr.rowcount > 0:
|
||||
_logger.info(f"✓ Updated dependencies in other modules: {cr.rowcount} row(s)")
|
||||
|
||||
_logger.info("=" * 70)
|
||||
_logger.info("MIGRATION COMPLETED: Module successfully renamed")
|
||||
_logger.info("Note: Model names (e.g., 'ows.machine') remain unchanged")
|
||||
_logger.info("=" * 70)
|
||||
else:
|
||||
_logger.info("INFO: Module 'open_workshop' not found - migration not needed")
|
||||
106
open_workshop_base/migrations/18.0.1.0.4/post-migration.py
Normal file
106
open_workshop_base/migrations/18.0.1.0.4/post-migration.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from datetime import date
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Post-Migration: Migriere bestehende ows.machine Daten zu maintenance.equipment
|
||||
Area wird in equipment.location gespeichert (nicht mehr in category!)
|
||||
"""
|
||||
_logger.info("=== Post-Migration: Daten-Migration zu equipment ===")
|
||||
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Zähle zu migrierende Maschinen
|
||||
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||
machines_to_migrate = cr.fetchone()[0]
|
||||
_logger.info(f"Found {machines_to_migrate} machines to migrate")
|
||||
|
||||
if machines_to_migrate == 0:
|
||||
_logger.info("No machines to migrate - skipping")
|
||||
return
|
||||
|
||||
_logger.info("Creating maintenance.equipment entries...")
|
||||
|
||||
# 2. Migriere alle Maschinen die noch kein equipment haben
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, name, area_id,
|
||||
active, create_uid, write_uid, create_date, write_date
|
||||
FROM ows_machine
|
||||
WHERE equipment_id IS NULL
|
||||
""")
|
||||
|
||||
machines = cr.fetchall()
|
||||
migrated_count = 0
|
||||
|
||||
for machine in machines:
|
||||
(m_id, m_name_jsonb, m_area_id,
|
||||
m_active, m_create_uid, m_write_uid, m_create_date, m_write_date) = machine
|
||||
|
||||
# Name aus JSONB extrahieren (de_DE oder en_US)
|
||||
m_name = None
|
||||
if m_name_jsonb:
|
||||
m_name = m_name_jsonb.get('de_DE') or m_name_jsonb.get('en_US')
|
||||
|
||||
# Serial number generieren
|
||||
m_serial_no = f"OWS-{m_id}"
|
||||
|
||||
# Location aus Area-Name ermitteln
|
||||
location = None
|
||||
if m_area_id:
|
||||
# Hole Area-Name (JSONB!) und nutze als location
|
||||
cr.execute("SELECT name FROM ows_machine_area WHERE id = %s", (m_area_id,))
|
||||
area_result = cr.fetchone()
|
||||
if area_result and area_result[0]:
|
||||
area_name_jsonb = area_result[0]
|
||||
if area_name_jsonb:
|
||||
location = area_name_jsonb.get('de_DE') or area_name_jsonb.get('en_US')
|
||||
|
||||
# Equipment erstellen (name als JSONB!)
|
||||
# Fallback: Wenn m_name leer, nutze serial_no
|
||||
display_name = m_name if m_name and m_name.strip() else m_serial_no
|
||||
|
||||
# Verwende SQL direkt für korrektes JSONB handling
|
||||
try:
|
||||
cr.execute("""
|
||||
INSERT INTO maintenance_equipment
|
||||
(name, serial_no, location, cost, effective_date, equipment_assign_to,
|
||||
active, create_uid, write_uid, create_date, write_date)
|
||||
VALUES
|
||||
(jsonb_build_object('de_DE', %s, 'en_US', %s), %s, %s, %s, %s, 'other',
|
||||
%s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (display_name, display_name, m_serial_no,
|
||||
location, 0.0, date.today(),
|
||||
m_active if m_active is not None else True,
|
||||
m_create_uid, m_write_uid, m_create_date, m_write_date))
|
||||
|
||||
equipment_id = cr.fetchone()[0]
|
||||
|
||||
# equipment_id in ows_machine setzen
|
||||
cr.execute("""
|
||||
UPDATE ows_machine
|
||||
SET equipment_id = %s
|
||||
WHERE id = %s
|
||||
""", (equipment_id, m_id))
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to migrate machine {m_id}: {e}")
|
||||
|
||||
# 3. Validierung
|
||||
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||
remaining = cr.fetchone()[0]
|
||||
|
||||
if remaining > 0:
|
||||
_logger.warning(f"⚠️ {remaining} machines could not be migrated!")
|
||||
else:
|
||||
_logger.info(f"✅ Successfully migrated {migrated_count} machines to equipment")
|
||||
|
||||
_logger.info("Post-migration completed")
|
||||
59
open_workshop_base/migrations/18.0.1.0.4/pre-migration.py
Normal file
59
open_workshop_base/migrations/18.0.1.0.4/pre-migration.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Pre-Migration: Bereite equipment_id Spalte vor
|
||||
Installiert maintenance-Modul falls nötig
|
||||
"""
|
||||
_logger.info("=== Pre-Migration: ows.machine → maintenance.equipment ===")
|
||||
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Prüfe ob maintenance installiert ist, falls nicht: installieren
|
||||
cr.execute("""
|
||||
SELECT state FROM ir_module_module
|
||||
WHERE name = 'maintenance'
|
||||
""")
|
||||
result = cr.fetchone()
|
||||
|
||||
if not result or result[0] != 'installed':
|
||||
_logger.info("Installing maintenance module...")
|
||||
maintenance_module = env['ir.module.module'].search([('name', '=', 'maintenance')])
|
||||
if maintenance_module:
|
||||
maintenance_module.button_immediate_install()
|
||||
_logger.info("✅ Maintenance module installed")
|
||||
else:
|
||||
raise Exception("Maintenance module not found in addons_path!")
|
||||
else:
|
||||
_logger.info("✓ Maintenance module already installed")
|
||||
|
||||
# 2. Spalte equipment_id hinzufügen (falls nicht vorhanden)
|
||||
cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'ows_machine' AND column_name = 'equipment_id'
|
||||
""")
|
||||
|
||||
if not cr.fetchone():
|
||||
_logger.info("Adding equipment_id column to ows_machine")
|
||||
cr.execute("""
|
||||
ALTER TABLE ows_machine
|
||||
ADD COLUMN equipment_id INTEGER;
|
||||
""")
|
||||
cr.execute("""
|
||||
ALTER TABLE ows_machine
|
||||
ADD CONSTRAINT ows_machine_equipment_id_fkey
|
||||
FOREIGN KEY (equipment_id)
|
||||
REFERENCES maintenance_equipment(id)
|
||||
ON DELETE CASCADE;
|
||||
""")
|
||||
_logger.info("✅ equipment_id column added")
|
||||
else:
|
||||
_logger.info("✓ equipment_id column already exists")
|
||||
|
||||
_logger.info("Pre-migration completed")
|
||||
|
|
@ -41,9 +41,10 @@ BEGIN
|
|||
|
||||
-- ===== MIGRATION DURCHFÜHREN =====
|
||||
|
||||
-- 1. Modulnamen aktualisieren
|
||||
-- 1. Modulnamen und Version aktualisieren
|
||||
UPDATE ir_module_module
|
||||
SET name = 'open_workshop_base'
|
||||
SET name = 'open_workshop_base',
|
||||
latest_version = '18.0.1.0.3' -- Auf 18.0.1.0.3 setzen, damit 18.0.1.0.4 Migration läuft
|
||||
WHERE name = 'open_workshop';
|
||||
|
||||
RAISE NOTICE '✓ Updated ir_module_module: % row(s)', (SELECT 1);
|
||||
|
|
@ -57,7 +58,13 @@ BEGIN
|
|||
SELECT COUNT(*) FROM ir_model_data WHERE module = 'open_workshop_base'
|
||||
);
|
||||
|
||||
-- 3. Abhängigkeiten in anderen Modulen aktualisieren (falls vorhanden)
|
||||
-- 3. Alten ir_model_data Eintrag für module_open_workshop löschen
|
||||
DELETE FROM ir_model_data
|
||||
WHERE module = 'base' AND name = 'module_open_workshop';
|
||||
|
||||
RAISE NOTICE '✓ Cleaned up old module_open_workshop entry';
|
||||
|
||||
-- 4. Abhängigkeiten in anderen Modulen aktualisieren (falls vorhanden)
|
||||
UPDATE ir_module_module_dependency
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop';
|
||||
|
|
|
|||
|
|
@ -440,12 +440,38 @@ class OwsMachineArea(models.Model):
|
|||
|
||||
|
||||
class OwsMachine(models.Model):
|
||||
"""
|
||||
Open Workshop Maschine - nutzt maintenance.equipment als Single Source of Truth.
|
||||
|
||||
Delegation Pattern (_inherits):
|
||||
- Beim Erstellen wird automatisch ein maintenance.equipment erstellt
|
||||
- Felder wie name, serial_no, cost etc. werden direkt von equipment übernommen
|
||||
- Keine Datenduplizierung!
|
||||
"""
|
||||
_name = 'ows.machine'
|
||||
_table = 'ows_machine'
|
||||
_description = 'OWS: Maschine'
|
||||
_inherits = {'maintenance.equipment': 'equipment_id'}
|
||||
|
||||
# PFLICHT: Verknüpfung zu maintenance.equipment (Single Source of Truth)
|
||||
equipment_id = fields.Many2one(
|
||||
'maintenance.equipment',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
string='Equipment',
|
||||
help='Verknüpfung zum Maintenance Equipment (Single Source of Truth für name, code, Preis, etc.)'
|
||||
)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
|
||||
# Delegierte Felder (kommen automatisch von equipment via _inherits):
|
||||
# - name (equipment.name)
|
||||
# - serial_no → wird als 'code' verwendet (siehe @api.depends unten)
|
||||
# - cost → purchase_price
|
||||
# - effective_date → purchase_date
|
||||
# - location → storage_location
|
||||
# - note → description
|
||||
# - category_id → Wird mit area_id synchronisiert
|
||||
|
||||
# OWS-spezifische Felder (nur in ows.machine!)
|
||||
category = fields.Selection([
|
||||
('green', 'Kategorie 1: grün'),
|
||||
('yellow', 'Kategorie 2: gelb'),
|
||||
|
|
@ -467,16 +493,11 @@ class OwsMachine(models.Model):
|
|||
}
|
||||
rec.category_icon = icon_map.get(rec.category, '⚪')
|
||||
|
||||
description = fields.Text()
|
||||
active = fields.Boolean(default=True)
|
||||
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
|
||||
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
|
||||
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
|
||||
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
|
||||
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
|
||||
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
|
||||
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
|
||||
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
|
||||
|
||||
@api.depends('product_ids.product_id.name')
|
||||
def _compute_product_using_names(self):
|
||||
|
|
@ -490,12 +511,63 @@ class OwsMachine(models.Model):
|
|||
names = machine.training_ids.mapped('training_id.name')
|
||||
machine.training_names = ", ".join(names)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.')
|
||||
]
|
||||
# Keine eigenen SQL Constraints - Equipment hat bereits unique constraint für serial_no
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
Beim Erstellen einer ows.machine:
|
||||
1. Equipment erstellen FALLS nicht vorhanden
|
||||
2. Area → Location synchronisieren
|
||||
"""
|
||||
# Nur Equipment erstellen, wenn keins ausgewählt wurde
|
||||
if 'equipment_id' not in vals or not vals['equipment_id']:
|
||||
equipment_vals = {
|
||||
'name': vals.get('name', 'Neue Maschine'),
|
||||
}
|
||||
equipment = self.env['maintenance.equipment'].create(equipment_vals)
|
||||
vals['equipment_id'] = equipment.id
|
||||
|
||||
# Area → Location Mapping
|
||||
if 'area_id' in vals and vals['area_id']:
|
||||
area = self.env['ows.machine.area'].browse(vals['area_id'])
|
||||
if area and area.name:
|
||||
# Setze location direkt auf area.name
|
||||
vals['location'] = area.name
|
||||
|
||||
return super(OwsMachine, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Bei Updates Area → Location synchronisieren
|
||||
"""
|
||||
if 'area_id' in vals and vals['area_id']:
|
||||
area = self.env['ows.machine.area'].browse(vals['area_id'])
|
||||
if area and area.name:
|
||||
vals['location'] = area.name
|
||||
|
||||
return super(OwsMachine, self).write(vals)
|
||||
|
||||
def name_get(self):
|
||||
return [(rec.id, f"{rec.name} ({rec.code})") for rec in self]
|
||||
result = []
|
||||
for rec in self:
|
||||
name = rec.name or 'Unbenannt'
|
||||
if rec.serial_no: # Kommt direkt von equipment via _inherits
|
||||
name = f"[{rec.serial_no}] {name}"
|
||||
result.append((rec.id, name))
|
||||
return result
|
||||
|
||||
def action_open_equipment(self):
|
||||
"""Smart Button: Öffnet die Equipment-Detailansicht"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Equipment Details',
|
||||
'res_model': 'maintenance.equipment',
|
||||
'res_id': self.equipment_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_access_list_grouped(self, partner_id):
|
||||
|
|
|
|||
|
|
@ -8,14 +8,11 @@
|
|||
<list>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="serial_no" string="Code"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id" widget="many2one_color"/>
|
||||
<field name="product_names"/>
|
||||
<field name="training_names"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
|
||||
</list>
|
||||
|
|
@ -29,19 +26,29 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Maschine">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_equipment" type="object" class="oe_stat_button" icon="fa-wrench">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Equipment</span>
|
||||
<span class="o_stat_text">Details</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
<group string="Open Workshop" name="ows_group">
|
||||
<field name="equipment_id"
|
||||
options="{'no_create': False, 'no_open': False}"
|
||||
context="{'default_name': name}"
|
||||
string="Equipment"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="category"/>
|
||||
<field name="area_id"/>
|
||||
</group>
|
||||
<group name="info_group">
|
||||
<field name="serial_no" readonly="1" string="Code (von Equipment)"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Notebook für Produkte und Einweisungen -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user