Project Maps, Trigger usw

This commit is contained in:
jopster 2024-07-08 14:09:56 +02:00
parent 13f556bf62
commit 3626b9c8c6
9 changed files with 568 additions and 0 deletions

205
attendance.xml Normal file
View File

@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<template xml:space="preserve">
<t t-name="PresenceIndicator">
<div id="oe_hr_attendance_status" class="fa fa-circle me-1 text-400 " role="img" aria-label="Available" title="Available">
</div>
</t>
<t t-name="HrAttendanceCardLayout">
<div class="o_hr_attendance_kiosk_mode_container o_home_menu_background d-flex flex-column align-items-center justify-content-center h-100 text-center">
<span class="o_hr_attendance_kiosk_backdrop position-absolute top-0 start-0 end-0 bottom-0 bg-black-25"/>
<div class="o_hr_attendance_clock bg-black-50 p-3 py-md-2 m-0 mt-md-5 me-md-5 h2 text-white font-monospace"/>
<div t-attf-class="o_hr_attendance_kiosk_mode flex-grow-1 flex-md-grow-0 card pb-3 px-0 px-lg-5 {{kioskModeClasses ? kioskModeClasses : '' }}">
<div class="card-body d-flex flex-column p-0 p-md-4">
<t t-out="bodyContent"></t>
</div>
</div>
</div>
</t>
<t t-name="HrAttendanceUserBadge">
<div class="o_hr_attendance_user_badge o_home_menu_background d-flex align-items-end justify-content-center flex-grow-1 pt-5 pt-md-4 bg-odoo">
<img class="img rounded-circle mb-n5" t-attf-src="/web/image?model=hr.employee.public&amp;field=avatar_128&amp;id=#{userId}" t-att-title="userName" height="80" t-att-alt="userName"/>
</div>
</t>
<t t-name="HrAttendanceCheckInOutButtons">
<div class="flex-grow-1">
<button t-attf-class="o_hr_attendance_sign_in_out_icon btn btn-{{ checked_in ? 'warning' : 'success' }} align-self-center px-5 py-3 mt-4 mb-2">
<span class="align-middle fs-2 me-3 text-white" t-if="!checked_in">Check IN</span>
<i t-attf-class="fa fa-4x fa-sign-{{ checked_in ? 'out' : 'in' }} align-middle"/>
<span class="align-middle fs-2 ms-3" t-if="checked_in">Check OUT</span>
</button>
</div>
</t>
<t t-name="HrAttendanceKioskMode">
<t t-call="HrAttendanceCardLayout">
<t t-set="kioskModeClasses" t-translation="off">o_barcode_main pt-5</t>
<t t-set="bodyContent">
<h2 class="mb-2"><small>Welcome to</small> <t t-esc="widget.company_name"/></h2>
<img t-attf-src="{{widget.company_image_url}}" alt="Company Logo" class="o_hr_attendance_kiosk_company_image align-self-center img img-fluid mb-3" width="200"/>
<div class="o_hr_attendance_kiosk_welcome_row d-flex flex-column pb-5">
<div class="col-md-5 mt-5 mb-5 mb-md-0 align-self-center" t-if="widget.kiosk_mode != 'manual'">
<img src="/barcodes/static/img/barcode.png" alt="Barcode" style="width: 115px;height: 60px"/>
<h6 class="mt-2 text-muted">Scan your badge</h6>
</div>
<div class="mt-5 align-self-end" t-if="widget.kiosk_mode == 'barcode_manual'">
<button class="o_hr_attendance_button_employees btn btn-link">
Identify Manually
</button>
</div>
<div class="mt-5 align-self-center" t-if="widget.kiosk_mode == 'manual'">
<button class="o_hr_attendance_button_employees btn btn-primary px-5 py-3 mt-4 mb-2">
<span class="fs-2">Identify Manually</span>
</button>
</div>
</div>
</t>
</t>
</t>
<t t-name="HrAttendanceMyMainMenu">
<t t-call="HrAttendanceCardLayout">
<t t-set="bodyContent">
<t t-if="widget.employee">
<t t-set="checked_in" t-value="widget.employee.attendance_state=='checked_in'"/>
<t t-call="HrAttendanceUserBadge">
<t t-set="userId" t-value="widget.employee.id"/>
<t t-set="userName" t-value="widget.employee.name"/>
</t>
<div class="flex-grow-1">
<h1 class="mt-5" t-esc="widget.employee.name"/>
<h3><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.hours_today"/></h4>
</div>
<t t-call="HrAttendanceCheckInOutButtons"/>
</t>
<div class="alert alert-warning" t-else="">
<b>Warning</b> : Your user should be linked to an employee to use attendance.<br/> Please contact your administrator.
</div>
</t>
</t>
</t>
<t t-name="HrAttendanceKioskConfirm">
<t t-call="HrAttendanceCardLayout">
<t t-set="bodyContent">
<t t-set="checked_in" t-value="widget.employee_state=='checked_in'"/>
<button class="o_hr_attendance_back_button btn btn-block btn-secondary btn-lg d-block d-md-none py-5">
<i class="fa fa-chevron-left me-2"/> Go back
</button>
<t t-if="widget.employee_id" t-call="HrAttendanceUserBadge">
<t t-set="userId" t-value="widget.employee_id"/>
<t t-set="userName" t-value="widget.employee_name"/>
</t>
<button class="o_hr_attendance_back_button o_hr_attendance_back_button_md btn btn-secondary d-none d-md-inline-flex align-items-center position-absolute top-0 start-0 rounded-circle">
<i class="fa fa-2x fa-fw fa-chevron-left me-1" role="img" aria-label="Go back" title="Go back"/>
</button>
<div t-if="widget.employee_id" class="flex-grow-1">
<h1 class="mt-5 mb8"><t t-esc="widget.employee_name"/></h1>
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.employee_hours_today"/></h4>
<t t-if="!widget.use_pin" t-call="HrAttendanceCheckInOutButtons"/>
<t t-else="">
<h3 class="mt-4 mb0 text-muted">Please enter your PIN to <b t-if="checked_in">check out</b><b t-else="">check in</b></h3>
<div class="row">
<div class="col-md-8 offset-md-2 o_hr_attendance_pin_pad">
<div class="row g-0" >
<div class="col-12 mb8 mt8">
<input class="o_hr_attendance_PINbox border-0 bg-white fs-1 text-center" type="password" disabled="true"/>
</div>
</div>
<div class="row g-0">
<t t-foreach="['1', '2', '3', '4', '5', '6', '7', '8', '9', ['C', 'btn-warning'], '0', ['ok', 'btn-primary']]" t-as="btn_name">
<div class="col-4 p-1">
<a href="#" t-attf-class="o_hr_attendance_PINbox_button btn {{btn_name[1]? btn_name[1] : 'btn-secondary border'}} btn-block btn-lg {{ 'o_hr_attendance_pin_pad_button_' + btn_name[0] }} d-flex align-items-center justify-content-center">
<t t-esc="btn_name[0]"/>
</a>
</div>
</t>
</div>
</div>
</div>
</t>
</div>
<div t-else="" class="alert alert-danger mx-3" role="alert">
<h4 class="alert-heading">Error: could not find corresponding employee.</h4>
<p>Please return to the main menu.</p>
</div>
<a role="button" class="oe_attendance_sign_in_out" aria-label="Sign out" title="Sign out"/>
</t>
</t>
</t>
<t t-name="HrAttendanceGreetingMessage">
<t t-call="HrAttendanceCardLayout">
<t t-set="bodyContent">
<t t-set="checked_in" t-value="widget.employee_state=='checked_in'"/>
<t t-if="widget.attendance">
<t t-call="HrAttendanceUserBadge">
<t t-set="userId" t-value="widget.attendance.employee_id[0]"/>
<t t-set="userName" t-value="widget.employee_name"/>
</t>
<div t-if="widget.attendance.check_out" class="flex-grow-1">
<h1 class="mt-5">Goodbye <t t-esc="widget.employee_name"/>!</h1>
<h2 class="o_hr_attendance_message_message mt4 mb24"/>
<div class="alert alert-info fs-2 mx-3" role="status">
Checked out at <b><t t-esc="widget.attendance.check_out_time"/></b>
<br/><b><t t-esc="widget.hours_today"/></b>
</div>
<div t-att-class="'alert ' + (widget.today_overtime_float &gt;= 0 ? 'alert-success' : 'alert-danger') + ' h3 mx-3'" role="status">
Extra hours today:
<span t-esc="widget.today_overtime"/>
</div>
<t t-if="widget.total_overtime_float &gt; 0">
Total extra hours: <span t-esc="widget.total_overtime"/>
</t>
<h3 class="o_hr_attendance_random_message fst-italic mb24"/>
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
</div>
<div t-else="" class="flex-grow-1">
<h1 class="mt-5 mb0">Welcome <t t-esc="widget.employee_name"/>!</h1>
<h2 class="o_hr_attendance_message_message mt4 mb24"/>
<div class="alert alert-info fs-2 mx-3" role="status">
Checked in at <b><t t-esc="widget.attendance.check_in_time"/></b>
</div>
<h3 class="o_hr_attendance_random_message mb24"/>
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
</div>
<div class="flex-grow-1">
<button class="o_hr_attendance_button_dismiss align-self-center btn btn-primary btn-lg px-5 py-3">
<span class="fs-2" t-if="widget.attendance.check_out">Goodbye</span>
<span class="fs-2" t-else="">OK</span>
</button>
</div>
</t>
<t t-else="">
<div class="flex-grow-1">
<div class="alert alert-warning mt-5 mx-3" role="alert">
<h4 class="alert-heading">Invalid request</h4>
<p>Please return to the main menu.</p>
</div>
</div>
<div class="flex-grow-1">
<button class="o_hr_attendance_button_dismiss btn btn-primary btn-lg fs-2 px-5 py-3">
<i class="fa fa-chevron-left me-2"/>
<span class="fs-2">Go back</span>
</button>
</div>
</t>
</t>
</t>
</t>
</template>

4
controllers/__init__.py Normal file
View File

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

53
controllers/main.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
import logging
from odoo.http import request
_logger = logging.getLogger(__name__)
from odoo.tools.json import scriptsafe
class GoogleMap(http.Controller):
'''
This class generates on-the-fly partner maps that can be reused in every
website page. To do so, just use an ``<iframe ...>`` whose ``src``
attribute points to ``/google_map`` (this controller generates a complete
HTML5 page).
URL query parameters:
- ``partner_ids``: a comma-separated list of ids (partners to be shown)
- ``partner_url``: the base-url to display the partner
(eg: if ``partner_url`` is ``/partners/``, when the user will click on
a partner on the map, it will be redirected to <myodoo>.com/partners/<id>)
In order to resize the map, simply resize the ``iframe`` with CSS
directives ``width`` and ``height``.
'''
@http.route(['/google_map'], type='http', auth="public", website=True, sitemap=False)
def google_map(self, *arg, **post):
projects = request.env['dss.projects'].sudo().search([('standort_visible', '=', True)])
_logger.info("Google Maps " + str(projects)+ " and Record : "+str(len(projects)))
projects_data = {
"counter": len(projects),
"projects": []
}
for project in projects.with_context(show_address=True):
projects_data["projects"].append({
'id': project.id,
'name': project.projektname,
'latitude': str(project.standort_lati) if project.standort_lati else False,
'longitude': str(project.standort_long) if project.standort_long else False,
})
if 'customers' in post.get('partner_url', ''):
partner_url = '/customers/'
else:
partner_url = '/partners/'
google_maps_api_key = request.website.google_maps_api_key
values = {
'partner_url': partner_url,
'partner_data': scriptsafe.dumps(projects_data),
'google_maps_api_key': google_maps_api_key,
}
return request.render("website_google_map.google_map", values)

158
hr_attendance_view.xml Normal file
View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- views -->
<record id="view_attendance_tree" model="ir.ui.view">
<field name="name">hr.attendance.tree</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<tree string="Employee attendances" editable="bottom" sample="1">
<field name="employee_id"/>
<field name="check_in"/>
<field name="check_out"/>
<field name="worked_hours" string="Work Hours" widget="float_time"/>
</tree>
</field>
</record>
<record id="view_hr_attendance_kanban" model="ir.ui.view">
<field name="name">hr.attendance.kanban</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" sample="1">
<field name="employee_id"/>
<field name="check_in"/>
<field name="check_out"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div>
<img t-att-src="kanban_image('hr.employee', 'avatar_128', record.employee_id.raw_value)" t-att-title="record.employee_id.value" t-att-alt="record.employee_id.value" class="oe_kanban_avatar o_image_24_cover mr4"/>
<span class="o_kanban_record_title">
<strong><t t-esc="record.employee_id.value"/></strong>
</span>
</div>
<hr class="mt4 mb8"/>
<div class="o_kanban_record_subtitle">
<i class="fa fa-calendar" aria-label="Period" role="img" title="Period"></i>
<t t-esc="record.check_in.value"/>
- <t t-esc="record.check_out.value"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="hr_attendance_view_form" model="ir.ui.view">
<field name="name">hr.attendance.form</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<form string="Employee attendances">
<sheet>
<group>
<field name="employee_id"/>
<field name="check_in"/>
<field name="check_out"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_attendance_view_filter" model="ir.ui.view">
<field name="name">hr_attendance_view_filter</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<search string="Hr Attendance Search">
<field name="employee_id"/>
<field name="department_id" operator="child_of"/>
<filter string="My Attendances" name="myattendances" domain="[('employee_id.user_id', '=', uid)]" />
<separator/>
<filter string="Check In" name="check_in_filter" date="check_in" default_period="last_month"/>
<filter string="No Check Out" name="nocheckout" domain="[('check_out', '=', False)]" />
<group expand="0" string="Group By">
<filter string="Employee" name="employee" context="{'group_by': 'employee_id'}"/>
<filter string="Check In" name="groupby_name" context="{'group_by': 'check_in'}"/>
<filter string="Check Out" name="groupby_check_out" context="{'group_by': 'check_out'}"/>
</group>
</search>
</field>
</record>
<!-- actions -->
<record id="hr_attendance_action" model="ir.actions.act_window">
<field name="name">Attendances</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">tree,kanban,form</field>
<field name="context">{"search_default_today":1}</field>
<field name="search_view_id" ref="hr_attendance_view_filter" />
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No attendance records found
</p><p>
The attendance records of your employees will be displayed here.
</p>
</field>
</record>
<record id="hr_attendance_action_employee" model="ir.actions.act_window">
<field name="name">Attendances</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">tree,form</field>
<field name="context">{'create': False}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No attendance records to display
</p><p>
The attendance records of your employees will be displayed here.
</p>
</field>
</record>
<record id="hr_attendance_action_overview" model="ir.actions.act_window">
<field name="name">Attendances</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">tree</field>
<field name="context">{'create': False}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No attendance records to display
</p><p>
Your attendance records will be displayed here.
</p>
</field>
</record>
<record id="hr_attendance_action_kiosk_mode" model="ir.actions.client">
<field name="name">Attendances</field>
<field name="tag">hr_attendance_kiosk_mode</field>
<field name="target">fullscreen</field>
</record>
<record id="hr_attendance_action_my_attendances" model="ir.actions.client">
<field name="name">Attendance</field>
<field name="tag">hr_attendance_my_attendances</field>
<field name="target">main</field>
</record>
<record id="hr_attendance_action_greeting_message" model="ir.actions.client">
<field name="name">Message</field>
<field name="tag">hr_attendance_greeting_message</field>
</record>
<!-- Menus -->
<menuitem id="menu_hr_attendance_root" name="Attendances" sequence="205" groups="hr_attendance.group_hr_attendance,hr_attendance.group_hr_attendance_kiosk" web_icon="hr_attendance,static/description/icon.svg"/>
<menuitem id="menu_hr_attendance_my_attendances" name="Check In / Check Out" parent="menu_hr_attendance_root" sequence="1" groups="hr_attendance.group_hr_attendance" action="hr_attendance_action_my_attendances"/>
<menuitem id="menu_hr_attendance_attendances_overview" name="Attendances" parent="menu_hr_attendance_root" sequence="1" groups="hr_attendance.group_hr_attendance" action="hr_attendance_action_overview"/>
<menuitem id="menu_hr_attendance_kiosk_no_user_mode" name="Kiosk Mode" parent="menu_hr_attendance_root" sequence="10" groups="hr_attendance.group_hr_attendance_kiosk" action="hr_attendance_action_kiosk_mode"/>
<menuitem id="menu_hr_attendance_view_attendances" name="Attendances" parent="menu_hr_attendance_root" sequence="10" groups="hr_attendance.group_hr_attendance_user" action="hr_attendance_action"/>
</odoo>

59
my_attendances.js Normal file
View File

@ -0,0 +1,59 @@
odoo.define('hr_attendance.my_attendances', function (require) {
"use strict";
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var field_utils = require('web.field_utils');
const session = require('web.session');
var MyAttendances = AbstractAction.extend({
contentTemplate: 'HrAttendanceMyMainMenu',
events: {
"click .o_hr_attendance_sign_in_out_icon": _.debounce(function() {
this.update_attendance();
}, 200, true),
},
willStart: function () {
var self = this;
var def = this._rpc({
model: 'hr.employee',
method: 'search_read',
args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name', 'hours_today']],
context: session.user_context,
})
.then(function (res) {
self.employee = res.length && res[0];
if (res.length) {
self.hours_today = field_utils.format.float_time(self.employee.hours_today);
}
});
return Promise.all([def, this._super.apply(this, arguments)]);
},
update_attendance: function () {
var self = this;
this._rpc({
model: 'hr.employee',
method: 'attendance_manual',
args: [[self.employee.id], 'hr_attendance.hr_attendance_action_my_attendances'],
context: session.user_context,
})
.then(function(result) {
if (result.action) {
self.do_action(result.action);
} else if (result.warning) {
self.displayNotification({ title: result.warning, type: 'danger' });
}
});
},
});
core.action_registry.add('hr_attendance_my_attendances', MyAttendances);
return MyAttendances;
});

61
res_partner.py Normal file
View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import config
class ResPartner(models.Model):
_inherit = "res.partner"
date_localization = fields.Date(string='Geolocation Date')
def write(self, vals):
# Reset latitude/longitude in case we modify the address without
# updating the related geolocation fields
if any(field in vals for field in ['street', 'zip', 'city', 'state_id', 'country_id']) \
and not all('partner_%s' % field in vals for field in ['latitude', 'longitude']):
vals.update({
'partner_latitude': 0.0,
'partner_longitude': 0.0,
})
return super().write(vals)
@api.model
def _geo_localize(self, street='', zip='', city='', state='', country=''):
geo_obj = self.env['base.geocoder']
search = geo_obj.geo_query_address(street=street, zip=zip, city=city, state=state, country=country)
result = geo_obj.geo_find(search, force_country=country)
if result is None:
search = geo_obj.geo_query_address(city=city, state=state, country=country)
result = geo_obj.geo_find(search, force_country=country)
return result
def geo_localize(self):
# We need country names in English below
if not self._context.get('force_geo_localize') \
and (self._context.get('import_file') \
or any(config[key] for key in ['test_enable', 'test_file', 'init', 'update'])):
return False
partners_not_geo_localized = self.env['res.partner']
for partner in self.with_context(lang='en_US'):
result = self._geo_localize(partner.street,
partner.zip,
partner.city,
partner.state_id.name,
partner.country_id.name)
if result:
partner.write({
'partner_latitude': result[0],
'partner_longitude': result[1],
'date_localization': fields.Date.context_today(partner)
})
else:
partners_not_geo_localized |= partner
if partners_not_geo_localized:
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
'title': _("Warning"),
'message': _('No match found for %(partner_names)s address(es).', partner_names=', '.join(partners_not_geo_localized.mapped('name')))
})
return True

BIN
static/images/dsscalc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
static/src/img/dsscalc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="google_map">
&lt;!DOCTYPE html&gt;
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<title>World Map</title>
<link rel="stylesheet" type="text/css" href="/website_google_map/static/src/css/website_google_map.css"/>
</head>
<body t-att-data-partner-url="partner_url or None">
<script>
var odoo_partner_data = <t t-out="partner_data"/>;
</script>
<div id="odoo-google-map"></div>
<t t-if="google_maps_api_key">
<script t-attf-src="//maps.google.com/maps/api/js?key=#{google_maps_api_key}"></script>
</t>
<t t-else="1">
<script src="//maps.google.com/maps/api/js"></script>
</t>
<script type="text/javascript" src="/website_google_map/static/src/lib/markerclusterer.js"></script>
<script type="text/javascript" src="/website_google_map/static/src/js/website_google_map.js"></script>
</body>
</html>
</template>
</odoo>