nocin.eu

Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[ABAP] Quick way to get last year’s BEGDA and ENDDA

Using method get_current_year of class cl_hcmfab_reporting_utility you can get BEGDA and ENDDA of the current year, e.g. 01.01.2025 and 31.12.2025

  CALL METHOD cl_hcmfab_reporting_utility=>get_current_year
    EXPORTING
      iv_date  = sy-datlo
    IMPORTING
      ev_begda = DATA(begda)
      ev_endda = DATA(endda).

I couldn’t find a similar method to get the same for the last year. But I could solve the problem with three simple lines.

DATA(last_year) = conv d( sy-datlo - 365 ).
DATA(begda)     = last_year(4) && '0101'.
DATA(endda)     = last_year(4) && '1231'.

After that, I asked Perplexity and it gave me a similar result.

DATA(lv_last_year) = sy-datum+0(4) - 1.
DATA(lv_begda)     = |{ lv_last_year }0101|.
DATA(lv_endda)     = |{ lv_last_year }1231|.

Using this method, you could even reduce it to two lines, and it would still be easy to read.

DATA(begda)  = |{ sy-datum(4) - 1 }0101|.
DATA(endda)  = |{ sy-datum(4) - 1 }1231|.

[CAP] Fiori Elements – SideEffects for actions

If you add a custom button in Fiori Elements to trigger an action (like described here), it can be necessary to reload after execution.

When having the following cds model (including a bound and an unbound action)

entity MyEntity as projection on my.Entity
        actions {
            action approval();
        };

action createMyEntity(description: String) returns Entity;

you can either trigger the whole entity to reload

# unbound
annotate myService.createMyEntity with
  @Common.SideEffects: {
    TargetEntities: ['/myService.EntityContainer/MyEntity']
  };

or just some specific fields

# bound
annotate myService.MyEntity.actions {
    approval @(Common.SideEffects: {TargetProperties: [
        'in/status',
        'in/modifiedBy',
        'in/modfiedOn',
    ]})
};

[Home Assistant] Zigbee Garagentoröffner

Wir haben zu unserem alten Haus auch eine alte Doppelgarage bekommen. Die Garage ist in einem Zustand, dass man sie eigentlich nur noch abreißen kann. Aufgrund des Alters ist sie so schmal, dass heute gängige Autos quasi gar nicht hineinpassen. Von den zwei Garagentoren ist immerhin eines elektrisch. Verbaut ist ein Genie Screw Drive Antrieb. Die Firma Genie gibt es heute noch, unser Modell ist aber sicher >30 Jahre alt und gibt es natürlich nicht mehr im Sortiment.

Das Tor öffnen kann man über einen Taster oder einer Fernbedienung. Dafür wurde zusätzlich noch ein Genie Radio Control Modul verbaut (der graue Kasten rechts neben der Steckdose). In der Steckdose ist praktischerweise der Garagentormotor angeschlossen. Und der weiße dünne Draht, der an dem Stromkabel nach rechts langläuft, führt zum Taster.

Die Fernbedienungen hatten mittlerweile beide das zeitliche gesegnet. Na gut, über eine sind wir auch mit einem Auto drübergefahren… 🙂

Interessanterweise gibt es den Dickert Handsender MAHS40-01 (10 Codierschalter) heute noch. Der Preis ist natürlich absolut lächerlich mit >30€.

Nach kurzer Recherche auf AliExpress habe mir daher lieber einen Zigbee Garagentoröffner gekauft für 13,08€.

Erheblich günstiger und nun kann die Garage auch in Home Assistant integriert werden. Erkannt wird der MANHOT Garage Door Opener in Zigbee2MQTT als Tuya TS603. Zwei Entitäten werden bereitgestellt, eine zum Öffnen und Schließen des Tores und eine weitere zeigt den aktuellen Status des Tores an.

Die Installation habe ich mir so einfach wie möglich gemacht. Einen Schönheitswettbewerb bei der Verkabelung gewinnt man in unserer abrissreifen Garage sowieso nicht mehr. Ich habe daher einfach den Draht vom Taster durchtrennt

und dann wieder mit zwei 3er WAGO Klemmen verbunden, sodass die Taster Funktion erhalten bleibt. Die WAGOs sind eigentlich etwas zu groß für den dünnen Draht, aber hatte gerade nichts Besseres zur Hand. Und auf eine Lüsterklemme hatte ich noch wenig Lust.

Das schwarze Kabel war bereits beim Zigbee Garagentoröffner beigelegt und habe ich in die verbliebenen freien WAGO Plätze gesteckt und mit dem anderen Ende in den Zigbee Schalter. Jetzt fehlte nur noch Strom und der Sensor für den Zustand des Tores (offen/geschlossen). Da ich die alte Verkabelung in der Aufputzdose nicht anrühren wollte, habe ich mir einfach einen 2-fach Steckdosenadapter genommen und für die 230v Stromversorgung des Schalters einen alten Stecker (mit viel zu langem Kabel), den ich irgendwo mal abgeschnitten hatte.

Jetzt noch den Sensor mit dem Zigbee Schalter verbunden und das andere Ende zum Tor geführt und drangeklebt (ja, das Tor müsste mal lackiert werden…).

Und das war es auch schon! Neben dem Taster und den Fernbedienungen kann das Tor nun auch via Home Assistant gesteuert werden.

An beiden Toren hatte ich bereits Tür/Fenster Kontaktsensoren, um einfach prüfen zu können, ob wir das Tor offen stehen lassen haben. Die Sensoren werden mit 2 x AAA-Batterien betrieben und müssen vermutlich alle 2-3 Jahre getauscht werden. Zumindest an einem Tor kann ich mir das jetzt aber sparen.

Eine geeignete Darstellung auf dem Dashboard fehlt mir allerdings noch.

    [YouTube] ReVanced Disable AI Translation

    Just learned from here, that you can get rid of those super annoying AI translations on YouTube ReVanced by changing the following settings:

    ReVanced Settings → Miscellaneous → Spoof Video Streams → Standard Client → Android VR → Standard Audio Language for VR → Select an uncommon language like Tamil, which is not supported

    It seems like, if it can’t translate to the chosen one, it uses the default language as “fallback”.

    I really wonder why YouTube does this. Does this AI autodub feature really lead to higher view counts? I can’t believe that.

    [SuccessFactors] Insert balances via TimeAccountDetail API

    I had the task to insert additional vacation days to the TimeAccount of some employees. To do this, you have to insert some new entries in the related TimeAccountDetail entity.

    The official documentation states that you can create a TimeAccountDetail via UPSERT, and there is even an example. A very bad formatted example….

    But to me, it was unclear, what I have to provide as externalCode. Obviously, TimeAccount_externalCode required the externalCode of the TimeAccount, which the entry should belong to. But I was expecting that the TimeAccountDetail externalCode should be created, when UPSERTing the entry. Somehow the documentation is missing some explanation here… But in the note 2243841 I found the missing hint:

    While using API calls, Successfactors system would not generate unique external Code for TimeAccountDetail API. You will have to explicitly provide external code which should be unique.

    Ok, so simply provide a random externalCode. Following my UPSERT Request:

    ### The related TimeAccount, where the entry should belong to. Can be fetched via another API call
    @TimeAccount_externalCode=aac183cdc57242cb9f7454131f0da069
    
    ### Create a random TimeAccountDetail externalCode 
    @externalCode=70b54990b44440fab4c5084c971cc744
    
    ### Insert balances
    GET {{$dotenv sf_api_url}}/odata/v2/upsert
    Authorization: Basic {{$dotenv sf_api_auth_base64}}
    Accept: application/json
    Content-Type: application/json
    
    { 
      "__metadata" : {
        "uri" : "http://my-sf-demo-api-server/odata/v2/TimeAccountDetail(TimeAccount_externalCode='{{TimeAccount_externalCode}}',externalCode='{{externalCode}}')", 
        "type" : "SFOData.TimeAccountDetail"
      },
      "TimeAccount_externalCode" : "{{TimeAccount_externalCode}}", 
      "externalCode" : "{{externalCode}}", 
      "bookingUnit" : "DAYS", 
      "bookingType" : "MANUAL_ADJUSTMENT", 
      "bookingDate" : "\/Date(1747872000000)\/", 
      "bookingAmount" : "1", 
      "comment" : "Urlaub"
    }
    

    But what ever I tried as externalCode, it always failed…

    In note 2243841, there is also the last sentence:

    We generally do not suggest to use Upsert operation for TimeAccountDetail OData API due to external code limitation.

    This confused me even more, but as the note is from 2016, I thought it could be outdated, and the official API documentation is correct. But it seems the opposite is right….
    When trying a normal POST instead of an UPSERT, it immediately worked.

    ### The related TimeAccount, where the entry should belong to. Can be fetched via another API call
    @TimeAccount_externalCode=aac183cdc57242cb9f7454131f0da069
    
    ### Create a random TimeAccountDetail externalCode 
    @externalCode=70b54990b44440fab4c5084c971cc744
    
    ### Insert balances
    POST {{$dotenv sf_api_url}}/odata/v2/TimeAccountDetail
    Authorization: Basic {{$dotenv sf_api_auth_base64}}
    Accept: application/json
    Content-Type: application/json
    
    { 
      "TimeAccount_externalCode" : "{{TimeAccount_externalCode}}", 
      "externalCode" : "{{externalCode}}", 
      "bookingUnit" : "DAYS", 
      "bookingType" : "MANUAL_ADJUSTMENT", 
      "bookingDate" : "\/Date(1747872000000)\/", 
      "bookingAmount" : "1", 
      "comment" : "Urlaub"
    }
    

    Not sure if I did something wrong when using UPSERT, but why should I even use an UPSERT, if I can simply use a normal POST request. Once again, poor SAP documentation has cost me a lot of my lifetime…

    [CAP] CQL Expand two levels deep

    I’ve already made a post about expanding compositions or associations (see here), but this time I had to extend two levels deep.

    My entity structure looked something to this:

    entity Header: cuid, managed {
        Items           : Composition of many Header.Items
                              on Items.purgeRun = $self;
    }
    
    entity Header.Items : cuid, managed {
        file            : Association to Files @mandatory @assert.target;
        header: Association to Header;
    }
    
    entity Files : cuid, managed {          
        content         : LargeBinary @Core.MediaType: mediaType  @Core.ContentDisposition.Filename: fileName  @Core.ContentDisposition.Type: 'inline';
        mediaType       : String      @Core.IsMediaType: true;
        fileName        : String      @mandatory;
        size            : Integer;
    }   
    

    It is possible to query all data in a single HTTP call using $expand twice.

    ### Header + Items + File
    GET {{server}}/odata/v4/service/MainEntity/{{ID}}
    ?$expand=Items($expand=file)
    Authorization: Basic {{username}} {{password}}
    

    However, if you want to fetch the same data using CQL, the following query is required:

            const data = await SELECT(Header, ID)
                .columns(h => {
                    h('*')
                    h.Items(i => {
                        i('*')
                        i.file(f => f('*'))
                    })
                })
    

    This syntax is really hard to remember…

    [Android] Taschenlampe einschalten durch seitliches Schütteln

    Durch Zufall habe ich letztens herausgefunden, dass man die Taschenlampe des Smartphones durch seitliches Schütteln einschalten kann, sofern man es in den Einstellungen aktiviert hat. Ich muss sagen, ich nutze es quasi täglich und es hat mein Leben verändert 😀

    Einstellungen → System → Touch-Gesten & Bewegungen → Moto-Gesten → Hacken (Taschenlampe mit der Hack-Geste ein-/ausschalten)

    [ABAP] Ermittle Sachkonto zu symbolischen Konto aus FI System

    DATA(service_manager) = NEW cl_hrpp_ws_service_manager( ).
    
    SELECT * INTO TABLE @DATA(lt_t52el) FROM t52el  WHERE molga = '01' AND symko <> @space AND endda >= @pn-begps. " Verknüpfung Lohnart zu symbolischen Konto
    SELECT * INTO TABLE @DATA(lt_t52ek) FROM t52ek.                                                                " Symbolische Konten
    SELECT * INTO TABLE @DATA(lt_t52ep) FROM t52ep.                                                                " Kontierungsarten
    
     LOOP AT lt_t52el INTO DATA(ls_t52el). " Ermittle je Lohnart die symbolischen Konten (1:n)
    
          TRY.
              DATA(ls_t52ek) = lt_t52ek[ symko = ls_t52el-symko ].   " Ermittele Kontierungsart
              DATA(ls_t52ep) = lt_t52ep[ koart = ls_t52ek-koart ].   " Ermittele Typ des symbolischen Kontos
              DATA(process)  = CONV ktosl( 'HR' && ls_t52ep-kttyp ).
            CATCH cx_sy_itab_line_not_found.
              CONTINUE.
          ENDTRY.
    
          service_manager->hrpp_fi_acct_det_hr( EXPORTING companycode        = pernr-bukrs
                                                          process            = process              " Vorgangsschlüssel
                                                          symb_acct          = ls_t52el-symko
                                                          eg_acct_det        = CONV #( '1' )        " Überleitung FI/CO: Mitarbeitergruppierung Kontenfindung: alle Mitarbeiter
                                                IMPORTING gl_account_debit   = DATA(account_debit)
                                                          gl_account_credit  = DATA(account_credit)
                                                          return_tab         = DATA(return_tab) ).
          IF line_exists( return_tab[ type = 'E' ] ).
            CONTINUE.
          ENDIF.
    
          " Lese Sachkonto Text
          service_manager->hrpp_gl_acc_getdetail( EXPORTING companycode    = pernr-bukrs
                                                            glacct         = account_debit
                                                            language       = sy-langu
                                                            text_only      = abap_true
                                                  IMPORTING account_detail = DATA(account_detail) ).
    
     ENDLOOP.