Kontrola nepřetržitého odpočinku v týdnu, část 1

Tomáš Holan       18. 2. 2013             5987 zobrazení

Nedávno jsem implementoval algoritmus pro kontrolu, zda rozvržení pracovních směn splňuje zákonnou dobu odpočinku mezi směnami a nepřetržitého odpočinku v týdnu dle legislativy České republiky. Algoritmus je součástí docházkového systému vyvíjeného naší společností. Myslím, že je tento algoritmus sám o sobě docela zajímavý, ale hlavně je to dobrý příklad přímo z praxe na demonstrací, jak může být netriviální celý proces od zadání (v tomto případě v podobě zákoníku práce) až po výsledný kód.

Vlastně se jedná o nezávislé kontroly dvě - tedy dva algoritmy, které vycházejí z popisu v zákoníku práce (zákon č. 262/2006 Hl. IV, díl 1) dle § 90 (nepřetržitý odpočinek mezi dvěma směnami) resp. § 92 (nepřetržitý odpočinek v týdnu). V této sérii uvedu pouze implementaci algoritmu druhého, protože je složitější a pro nás tudíž i zajímavější.

Nejprve vás stručně seznámím s k tomuto relevantními záležitostmi týkajících se docházkového systému. V systému existují tzv. kalendáře. Každá osoba má aktuálně přiřazen jeden konkrétní kalendář. Každý kalendář jednak obsahuje definici pracovních směn a jednak umožňují nastavit konkrétní hodnoty v systému definovaných parametrů, které ovlivňují vlastní zpracování docházky pro danou osobu. Každá osoba pak ještě může mít definované nějaké výjimky kalendáře tj. změny v definici pracovních směn oproti kalendáři. Business vrstva aplikační logiky pak umožňuje:

  • Pro každou osobu a měsíc vrátit pracovní směny (s datem a časem začátku i konce dané směny) včetně zahrnutí všech výjimek kalendáře pro jednotlivé dny měsíce.
  • Pro každou osobu, datum a název parametru vrátit konkrétní hodnotu parametru kalendáře.

Z § 92 zákoníku práce je pro vlastní algoritmus důležité pouze toto:

  • Zaměstnavatel je povinen rozvrhnout pracovní dobu tak, aby zaměstnanec měl nepřetržitý odpočinek v týdnu během každého období sedmi po sobě jdoucích kalendářních dnů (*) v trvání alespoň 35 hodin (u mladistvého zaměstnance je tato hodnota 48 hodin).
  • V některých případech (**) lze tento nepřetržitý odpočinek v týdnu zaměstnancům zkrátit až na 24 hodiny s tím, že zaměstnancům bude poskytnut nepřetržitý odpočinek v týdnu tak, aby za období 2 týdnů činila délka tohoto odpočinku celkem alespoň 70 hodin (***) (toto ustanovení neplatí pro mladistvé).

A můžeme pomalu začít. Vlastní kontrola bude funkce, která se bude spouštět pro konkrétní osobu (zaměstnance) identifikovanou pomoci svého IDOsoby a dále měsíc/rok. Výstupem bude kolekce údajů o nalezeném nesouladu s legislativou. Tyto údaje budou:

  • Datum od a datum do určující, k jakému časovému intervalu se nesoulad vztahuje (pro tuto kontrolu se bude vždy jednat o týden).
  • Rozlišení, o který nesoulad se jedná (pro tuto kontrolu půjde vždy o nepřetržitý odpočinek v týdnu).
  • Popis nesouladu pro zobrazení do uživatelského rozhraní. Ten bude realizovaný jako funkce (Func<string>) z toho důvodu, aby byla prováděna jeho lokalizace do příslušné kultury až v momentě jeho zobrazování v uživatelském rozhraní (resp. přesněji se tak například bude dít v momentě jeho serializace na klienta).
/// <summary>
/// Typ kontroly zákonné doby odpočinku
/// </summary>
public enum PracovniCasyValidationType
{
    /// <summary>
    /// Nepřetržitý odpočinek v týdnu
    /// </summary>
    NepretrzityOdpocinekVTydnu = 1
}

/// <summary>
/// Konkrétní chyba při kontrole zákonné doby odpočinku
/// </summary>
public sealed class PracovniCasyValidationError
{
    #region member varible and default property initialization
    public PracovniCasyValidationType Type { get; private set; }
    public DateTime DatumOd { get; private set; }
    public DateTime DatumDo { get; private set; }

    private Func<string> PopisSelector;
    #endregion

    #region constructors and destructors
    internal PracovniCasyValidationError(PracovniCasyValidationType type, DateTime datumOd, DateTime datumDo, Func<string> popisSelector)
    {
        this.Type = type;
        this.DatumOd = datumOd;
        this.DatumDo = datumDo;
        this.PopisSelector = popisSelector;
    }
    #endregion

    #region action methods
    public override string ToString()
    {
        return GetPopis();
    }

    public string GetPopis()
    {
        return this.PopisSelector();
    }
    #endregion
}

/// <summary>
/// Kontrola dodržení zákonné doby odpočinku
/// </summary>
public static class PracovniCasyValidator
{
    #region action methods
    public static IEnumerable<PracovniCasyValidationError> Validate(int IDOsoby, int rok, int mesic)
    {
        return KontrolaNepretrzityOdpocinekVTydnu(IDOsoby, rok, mesic);
    }
    #endregion

    #region private member functions
    private static IEnumerable<PracovniCasyValidationError> KontrolaNepretrzityOdpocinekVTydnu(int IDOsoby, int rok, int mesic)
    {
        //TODO: Kontrola nepřetržitého odpočinku v týdnu
    }
    #endregion
}

Z hlediska případné změny časů pro různé režimy nebo budoucí změny legislativy a hlavně z důvodů vyřešení odlišných podmínek pro mladistvé budou konkrétní hodnoty, které se vyskytují ve výše uvedeném změní ze zákoníku, zavedeny jako parametry kalendáře a uvedené hodnoty budou jejich výchozí hodnoty. Bude se jednat o tyto parametry:

  • nepretrzityOdpocinek - Nepřetržitý odpočinek (35 hodin)
  • nepretrzityOdpocinekMin - Minimální nepřetržitý odpočinek (24 hodin)
  • nepretrzityOdpocinekDodatecny - Nepřetržitý odpočinek dodatečný (70 hodin)

Kód kterým půjde tyto hodnoty získat bude následující:

int nepretrzityOdpocinek = Nastaveni.Get().GetInt("NepretrzityOdpocinek", IDOsoby, new DateTime(rok, mesic, 1));                    //35
int nepretrzityOdpocinekMin = Nastaveni.Get().GetInt("NepretrzityOdpocinekMin", IDOsoby, new DateTime(rok, mesic, 1));              //24
int nepretrzityOdpocinekDodatecny = Nastaveni.Get().GetInt("NepretrzityOdpocinekDodatecny", IDOsoby, new DateTime(rok, mesic, 1));  //70

Kde objekt business vrstvy Nastavení můžeme simulovat takto:

internal sealed class Nastaveni
{
    #region member varible and default property initialization
    private static readonly Nastaveni s_instance = new Nastaveni();
    #endregion

    #region constructors and destructors
    private Nastaveni() { }
    #endregion

    #region action methods
    public static Nastaveni Get()
    {
        return s_instance;
    }

    public int GetInt(string parametr, int IDOsoby, DateTime datum)
    {
        switch (parametr)
        {
            case "NepretrzityOdpocinek":
                return 35;
            case "NepretrzityOdpocinekMin":
                return 24;
            case "NepretrzityOdpocinekDodatecny":
                return 70;
        }

        throw new ArgumentException("Parametr kalendáře '{0}' není definován.");
    }
    #endregion
}

Protože algoritmus bude muset procházet pracovní směny dané osoby, připravíme si nyní ještě i toto volání business vrstvy. Jak bylo výše uvedeno business vrstva umožňuje pro osobu a měsíc/rok získat směny pro jednotlivé dny měsíce.

Volání, kterým lze například získat posloupnost všech pracovních směn v měsíci vypadá pak následovně:

var smeny = from den in PracovniCasyOsobaMesic.Get(IDOsoby, rok, mesic).JednotliveDny
            from smena in den.Smeny
            select smena;

Objekt business vrstvy PracovniCasyOsobaMesic budeme opět jen simulovat, například takto:

[System.Diagnostics.DebuggerDisplay("ZnackaSmeny = {ZnackaSmeny}, DatumACasOd = {DatumACasOd}, DatumACasDo = {DatumACasDo}")]
internal sealed class PracovniCasyOsobaDenSmena
{
    #region member varible and default property initialization
    public string ZnackaSmeny { get; private set; }
    public DateTime DatumACasOd { get; private set; }
    public DateTime DatumACasDo { get; private set; }
    #endregion

    #region constructors and destructors
    internal PracovniCasyOsobaDenSmena(string znackaSmeny, DateTime datumACasOd, DateTime datumACasDo)
    {
        this.ZnackaSmeny = znackaSmeny;
        this.DatumACasOd = datumACasOd;
        this.DatumACasDo = datumACasDo;
    }
    #endregion
}

[System.Diagnostics.DebuggerDisplay("Datum = {Datum}, Smeny = {Smeny.Count}")]
internal sealed class PracovniCasyOsobaDen
{
    #region member varible and default property initialization
    public DateTime Datum { get; private set; }
    public IList<PracovniCasyOsobaDenSmena> Smeny { get; private set; }
    #endregion

    #region constructors and destructors
    internal PracovniCasyOsobaDen(DateTime datum, List<PracovniCasyOsobaDenSmena> smeny)
    {
        this.Datum = datum;
        this.Smeny = smeny.AsReadOnly();
    }
    #endregion
}

internal class PracovniCasyOsobaMesic
{
    #region member varible and default property initialization
    public IList<PracovniCasyOsobaDen> JednotliveDny { get; private set; }
    #endregion

    #region constructors and destructors
    private PracovniCasyOsobaMesic(List<PracovniCasyOsobaDen> jednotliveDny)
    {
        this.JednotliveDny = jednotliveDny.AsReadOnly();
    }
    #endregion

    #region action methods
    public static PracovniCasyOsobaMesic Get(int IDOsoby, int rok, int mesic)
    {
        string rozpisSmen = "R--RRRRR-RRRRRRRRRRRR-RRRRR";

        return FromRozpisSmen(rok, mesic, rozpisSmen);
    }
    #endregion

    #region private member functions
    private static PracovniCasyOsobaMesic FromRozpisSmen(int rok, int mesic, string rozpisSmen)
    {
        DateTime datumOd = new DateTime(rok, mesic, 1);
        DateTime datumDo = datumOd.AddMonths(1).AddDays(-1);

        var typSmen = new Dictionary<string, Tuple<TimeSpan, TimeSpan>>() 
        {
            { "R", Tuple.Create(new TimeSpan(8, 0, 0), new TimeSpan(16, 0, 0)) }
            { "V", Tuple.Create(new TimeSpan(8, 0, 0), new TimeSpan(18, 0, 0)) }
        };

        var jednotliveDny = new List<PracovniCasyOsobaDen>(31);
        int index = 0;

        for (DateTime datum = datumOd; datum <= datumDo; datum = datum.AddDays(1))
        {
            var smeny = new List<PracovniCasyOsobaDenSmena>();

            string znackaSmeny = rozpisSmen[index++ % rozpisSmen.Length].ToString();
            Tuple<TimeSpan, TimeSpan> casy;
            if (typSmen.TryGetValue(znackaSmeny, out casy))
            {
                smeny.Add(new PracovniCasyOsobaDenSmena(znackaSmeny, datum.Add(casy.Item1), datum.Add(casy.Item2)));
            }

            jednotliveDny.Add(new PracovniCasyOsobaDen(datum, smeny));
        }

        return new PracovniCasyOsobaMesic(jednotliveDny);
    }
    #endregion
}

Při této simulaci bude mít každý den v měsíci pouze jednu nebo žádnou směnu. V reálu to tak být nemusí, ale pro ověření našeho algoritmu je toto plně postačující.

Příště budeme pokračovat již implementací vlastního algoritmu.


(*) Z původního znění zákoníku práce byla tato klauzule „během každého období sedmi po sobě jdoucích kalendářních dnů“ vypuštěna. Jak s tímto naložíme se dozvíme později.

(**) To v kterých případech konkrétně pro nás není až tak důležité, důležité je ale to, že musíme uvažovat, že mohou existovat různé režimy a pro některé se tato podmínka má a pro některé nemá uplatnit. To samé platí ohledně rozlišení, zda je zaměstnanec mladistvý či nikoliv.

(***) Tento výklad navíc doplňuje: První odpočinek 24 hodin, druhý odpočinek 46 hodin (24 + 35 + 11=70).

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback