jurgendn commited on
Commit
1748405
1 Parent(s): 2c64d93

[Build] First time build

Browse files
Files changed (9) hide show
  1. .gitignore +160 -0
  2. README.md +0 -2
  3. app.py +41 -0
  4. cfg/config.yml +15 -0
  5. config.py +6 -0
  6. hr_utils/converter.py +51 -0
  7. hr_utils/entities.py +149 -0
  8. requirements.txt +3 -0
  9. type_alias.py +7 -0
.gitignore ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
README.md CHANGED
@@ -8,5 +8,3 @@ sdk_version: 3.28.3
8
  app_file: app.py
9
  pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
8
  app_file: app.py
9
  pinned: false
10
  ---
 
 
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+
3
+ import gradio as gr
4
+ import pandas as pd
5
+
6
+ from config import CONFIG
7
+ from hr_utils.converter import get_data, make_table, parse
8
+ from type_alias import GRADIO_OUTPUT
9
+
10
+ WORKING_TIME = CONFIG.WORKING_TIME
11
+ LATE = CONFIG.LATE
12
+
13
+
14
+ def calculate(file: tempfile._TemporaryFileWrapper) -> GRADIO_OUTPUT:
15
+ filename: str = file.name
16
+ data: pd.DataFrame = pd.read_excel(filename)
17
+ data = get_data(data=data)
18
+ employees_list = parse(data=data)
19
+ table = make_table(employees=employees_list)
20
+ with tempfile.NamedTemporaryFile(mode="w+b", suffix=".xlsx",
21
+ delete=False) as f:
22
+ table.to_excel(f, index=False)
23
+ downloadable_filename = f.name
24
+ return downloadable_filename, table
25
+
26
+
27
+ with gr.Blocks() as app:
28
+ with gr.Row():
29
+ upload_file = gr.File(label="Upload file here")
30
+ with gr.Row():
31
+ button = gr.Button(value="Calculate Working Time")
32
+ with gr.Row():
33
+ download_processed_file = gr.File()
34
+ with gr.Row():
35
+ processed_file = gr.DataFrame()
36
+
37
+ button.click(fn=calculate,
38
+ inputs=[upload_file],
39
+ outputs=[download_processed_file, processed_file])
40
+
41
+ app.launch()
cfg/config.yml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TIME_FORMAT: "%H:%M"
2
+
3
+ WORKING_TIME:
4
+ TOTAL_WORKING_TIME: 480
5
+ MORNING_IN: "08:00"
6
+ MORNING_OUT: "12:00"
7
+ AFTERNOON_IN: "13:00"
8
+ AFTERNOON_OUT: "17:00"
9
+ ROUND_GAP: 15
10
+
11
+ LATE:
12
+ MORNING_IN: 30
13
+ AFTERNOON_IN: 30
14
+ MORNING_OUT: 0
15
+ AFTERNOON_OUT: 0
config.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from dynaconf import Dynaconf
2
+
3
+ CONFIG = Dynaconf(
4
+ envvar_prefix="DYNACONF", # export envvars with `export DYNACONF_FOO=bar`.
5
+ settings_files=['./cfg/config.yml'], # Load files in the given order.
6
+ )
hr_utils/converter.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List
2
+
3
+ import pandas as pd
4
+
5
+ from config import CONFIG
6
+
7
+ from .entities import Employee, WorkingTime
8
+
9
+
10
+ def get_data(data: pd.DataFrame) -> pd.DataFrame:
11
+ data.columns = data.iloc[1, :]
12
+ data = data.iloc[2:, ]
13
+ data = data[['Mã N.Viên', "Tên nhân viên", "Ngày", "Vào", "Ra"]]
14
+ data = data.groupby(by=['Mã N.Viên', 'Ngày']).aggregate({
15
+ "Tên nhân viên": max,
16
+ "Vào": sum,
17
+ "Ra": sum
18
+ }).reset_index()
19
+ return data
20
+
21
+
22
+ def parse(data: pd.DataFrame) -> List[Employee]:
23
+ employees: List[Employee] = []
24
+ for (id, name), v in data.groupby(by=['Mã N.Viên', 'Tên nhân viên']):
25
+ employee = Employee(id=id, name=name, workdate={})
26
+ for val in v.values:
27
+ working_time = WorkingTime(date=val[1],
28
+ checkin=val[3],
29
+ checkout=val[4],
30
+ config=CONFIG)
31
+ date, work_time = working_time.to_tuple()
32
+ employee.workdate[date] = work_time
33
+ employees.append(employee)
34
+ return employees
35
+
36
+
37
+ def make_table(employees: List[Employee]) -> pd.DataFrame:
38
+ id_list: List[str] = []
39
+ name_list: List[str] = []
40
+ work_date: List[Dict] = []
41
+ for employee in employees:
42
+ id_list.append(employee.id)
43
+ name_list.append(employee.name)
44
+ work_date.append(employee.workdate)
45
+ work_df: pd.DataFrame = pd.DataFrame(work_date, dtype=int)
46
+ info_df: pd.DataFrame = pd.DataFrame({
47
+ 'Mã nhân viên': id_list,
48
+ 'Tên nhân viên': name_list
49
+ })
50
+ working_table: pd.DataFrame = pd.concat(objs=[info_df, work_df], axis=1)
51
+ return working_table
hr_utils/entities.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Dict, List
3
+
4
+ import pandas as pd
5
+ from dateutil.parser import parse
6
+ from type_alias import EARLY_MODE, LATE_MODE
7
+
8
+
9
+ class WorkingTime(object):
10
+
11
+ def __init__(self, date: datetime, checkin: str, checkout: str,
12
+ config) -> None:
13
+ self.MORNING_IN = parse(config.WORKING_TIME.MORNING_IN)
14
+ self.MORNING_OUT = parse(config.WORKING_TIME.MORNING_OUT)
15
+ self.AFTERNOON_IN = parse(config.WORKING_TIME.AFTERNOON_IN)
16
+ self.AFTERNOON_OUT = parse(config.WORKING_TIME.AFTERNOON_OUT)
17
+ self.LATE_TIME: Dict[str, int] = dict(
18
+ morning_in=config.LATE.MORNING_IN,
19
+ morning_out=config.LATE.MORNING_OUT,
20
+ afternoon_in=config.LATE.AFTERNOON_IN,
21
+ afternoon_out=config.LATE.AFTERNOON_OUT)
22
+ self.ROUND_GAP = config.WORKING_TIME.ROUND_GAP
23
+
24
+ self.date = datetime.strftime(date, "%d/%m/%Y")
25
+
26
+ self.error: bool = False
27
+ if checkin == 0:
28
+ # self.checkin = -1
29
+ self.error = True
30
+ else:
31
+ self.checkin = parse(checkin)
32
+ if self.checkin < self.MORNING_IN:
33
+ self.checkin = parse("08:00")
34
+
35
+ if checkout == 0:
36
+ # self.checkout = -1
37
+ self.error = True
38
+ else:
39
+ self.checkout = parse(checkout)
40
+ if self.checkout > self.AFTERNOON_OUT:
41
+ self.checkout = parse("17:00")
42
+
43
+ def __repr__(self) -> str:
44
+ return "{" + ";\n".join(f"{k}={getattr(self, k)}"
45
+ for k in self.__dict__) + "}"
46
+
47
+ def get_late_checkin(self, time: datetime, mode: LATE_MODE) -> int:
48
+ is_penalty_count: int = 0
49
+ if mode == 'morning_in':
50
+ target_time: datetime = self.MORNING_IN
51
+ acceptable: int = self.LATE_TIME.get('MORNING_IN', 30)
52
+ elif mode == 'afternoon_in':
53
+ target_time: datetime = self.AFTERNOON_IN
54
+ acceptable: int = self.LATE_TIME.get('AFTERNOON_IN', 30)
55
+ if time < target_time:
56
+ return 0
57
+ late_time = (time - target_time).seconds // 60
58
+ if late_time > acceptable:
59
+ is_penalty_count = 1
60
+ return is_penalty_count * late_time
61
+
62
+ def get_early_checkout(self, time: datetime, mode: EARLY_MODE) -> int:
63
+ is_penalty_count: int = 0
64
+ if mode == 'morning_out':
65
+ target_time: datetime = self.MORNING_OUT
66
+ acceptable: int = self.LATE_TIME.get('MORNING_OUT', 0)
67
+ elif mode == 'afternoon_out':
68
+ target_time: datetime = self.AFTERNOON_OUT
69
+ acceptable: int = self.LATE_TIME.get('AFTERNOON_OUT', 0)
70
+ if time > target_time:
71
+ return 0
72
+ early_time = (target_time - time).seconds // 60
73
+ if early_time > acceptable:
74
+ is_penalty_count = 1
75
+ return is_penalty_count * early_time
76
+
77
+ def morning_shift(self):
78
+ if self.error:
79
+ return -1
80
+ if self.checkin > self.MORNING_OUT:
81
+ return 0
82
+ late_checkin_penalty = self.get_late_checkin(time=self.checkin,
83
+ mode='morning_in')
84
+ early_checkout_penalty = self.get_early_checkout(time=self.checkout,
85
+ mode='morning_out')
86
+ return (self.MORNING_OUT - self.MORNING_IN
87
+ ).seconds // 60 - late_checkin_penalty - early_checkout_penalty
88
+
89
+ def afternoon_shift(self):
90
+ if self.error:
91
+ return -1
92
+ if self.checkout < self.AFTERNOON_IN:
93
+ return 0
94
+ late_checkin_penalty = self.get_late_checkin(time=self.checkin,
95
+ mode='afternoon_in')
96
+ early_checkout_penalty = self.get_early_checkout(time=self.checkout,
97
+ mode='afternoon_out')
98
+ return (self.AFTERNOON_OUT - self.AFTERNOON_IN
99
+ ).seconds // 60 - late_checkin_penalty - early_checkout_penalty
100
+
101
+ def working_time(self):
102
+ return self.morning_shift() + self.afternoon_shift()
103
+
104
+ def normalize(self, num: int) -> int:
105
+ gap = 15
106
+ return round(num / gap) * gap
107
+
108
+ def to_tuple(self):
109
+ if self.error:
110
+ return self.date, -1
111
+ return self.date, self.normalize(self.working_time())
112
+
113
+
114
+ class Employee(object):
115
+
116
+ def __init__(self,
117
+ id: str,
118
+ name: str,
119
+ workdate: Dict[str, int] = {}) -> None:
120
+ self.id = id
121
+ self.name = name
122
+ self.workdate = workdate
123
+
124
+ def __repr__(self) -> str:
125
+ return "{" + ";\n".join(f"{k}={getattr(self, k)}"
126
+ for k in self.__dict__) + "}"
127
+
128
+
129
+ class WorkingTable(object):
130
+
131
+ def __init__(self) -> None:
132
+ pass
133
+
134
+ def make_table(self, employees: List[Employee]):
135
+ id_list: List[str] = []
136
+ name_list: List[str] = []
137
+ work_date: List[Dict] = []
138
+ for employee in employees:
139
+ id_list.append(employee.id)
140
+ name_list.append(employee.name)
141
+ work_date.append(employee.workdate)
142
+ work_df: pd.DataFrame = pd.DataFrame(work_date, dtype=int)
143
+ info_df: pd.DataFrame = pd.DataFrame({
144
+ 'Mã nhân viên': id_list,
145
+ 'Tên nhân viên': name_list
146
+ })
147
+ working_table: pd.DataFrame = pd.concat(objs=[info_df, work_df],
148
+ axis=1)
149
+ return working_table
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pandas==1.5.3
2
+ openpyxl==3.0.10
3
+ dynaconf==3.1.12
type_alias.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from typing import Literal, Tuple
2
+
3
+ import pandas as pd
4
+
5
+ LATE_MODE = Literal['morning_in', 'afternoon_in']
6
+ EARLY_MODE = Literal['morning_out', 'afternoon_out']
7
+ GRADIO_OUTPUT = Tuple[str, pd.DataFrame]