odoo-addon-account-move-payroll-import 16.0.1.0.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. odoo/addons/account_move_payroll_import/CHANGELOG.md +14 -0
  2. odoo/addons/account_move_payroll_import/README.rst +117 -0
  3. odoo/addons/account_move_payroll_import/__init__.py +4 -0
  4. odoo/addons/account_move_payroll_import/__manifest__.py +39 -0
  5. odoo/addons/account_move_payroll_import/data/payroll_import_defaults.xml +108 -0
  6. odoo/addons/account_move_payroll_import/i18n/ca_ES.po +828 -0
  7. odoo/addons/account_move_payroll_import/i18n/es.po +829 -0
  8. odoo/addons/account_move_payroll_import/models/__init__.py +6 -0
  9. odoo/addons/account_move_payroll_import/models/account_move.py +167 -0
  10. odoo/addons/account_move_payroll_import/models/payroll_custom_concept.py +57 -0
  11. odoo/addons/account_move_payroll_import/models/payroll_import_mapping.py +28 -0
  12. odoo/addons/account_move_payroll_import/models/payroll_import_setup.py +497 -0
  13. odoo/addons/account_move_payroll_import/security/ir.model.access.csv +5 -0
  14. odoo/addons/account_move_payroll_import/static/src/css/styles.css +26 -0
  15. odoo/addons/account_move_payroll_import/static/src/js/payroll_import_button.js +32 -0
  16. odoo/addons/account_move_payroll_import/static/src/xml/payroll_import_templates.xml +24 -0
  17. odoo/addons/account_move_payroll_import/utils/__init__.py +0 -0
  18. odoo/addons/account_move_payroll_import/utils/file_utils.py +31 -0
  19. odoo/addons/account_move_payroll_import/utils/parse_utils.py +34 -0
  20. odoo/addons/account_move_payroll_import/views/assets_template.xml +13 -0
  21. odoo/addons/account_move_payroll_import/views/payroll_import_views.xml +229 -0
  22. odoo/addons/account_move_payroll_import/wizards/__init__.py +3 -0
  23. odoo/addons/account_move_payroll_import/wizards/payroll_import_wizard.py +320 -0
  24. odoo/addons/account_move_payroll_import/wizards/payroll_import_wizard.xml +44 -0
  25. odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/METADATA +131 -0
  26. odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/RECORD +28 -0
  27. odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/WHEEL +6 -0
  28. odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,229 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <odoo>
3
+ <data>
4
+ <record id="payroll_import_config" model="ir.ui.view">
5
+ <field name="name">Account Move Payroll Import Setup</field>
6
+ <field name="model">payroll.import.setup</field>
7
+ <field name="arch" type="xml">
8
+ <form string="Account Move Payroll Import Setting">
9
+ <sheet>
10
+ <div class="oe_title">
11
+ <label for="name" class="oe_edit_only" />
12
+ <h1 class="oe_primary">
13
+ <field name="name" class="oe_inline" />
14
+ </h1>
15
+ </div>
16
+ <div class="oe_title">
17
+ <h1 class="oe_primary_color">
18
+ Structure
19
+ </h1>
20
+ <p class="oe_grey oe_small_text">
21
+ Indicate the column number for each field, define accounts to
22
+ link move lines to and aggregate lines by employee.
23
+ </p>
24
+ </div>
25
+ <group>
26
+ <group>
27
+ <field name="header_lines" string="Header Lines" />
28
+ <field name="header_ref_line" string="Header Reference Line" />
29
+ </group>
30
+ <group>
31
+ <field name="journal_id" string="Journal" required="1" />
32
+ </group>
33
+ </group>
34
+
35
+ <div class="oe_title">
36
+ <h2 class="oe_primary_color">
37
+ Employee
38
+ </h2>
39
+ </div>
40
+ <group>
41
+ <group>
42
+ <field name="column_employee_id" string="Column Number" />
43
+ </group>
44
+ <group>
45
+ <!-- Empty Column (Right) -->
46
+ </group>
47
+ </group>
48
+
49
+ <div class="oe_title">
50
+ <h2 class="oe_primary_color">
51
+ Gross
52
+ </h2>
53
+ </div>
54
+ <group>
55
+ <group>
56
+ <field name="column_gross" string="Column Number" />
57
+ <field name="gross_account_id" string="Account" />
58
+ <field name="gross_tax_id" string="Tax" />
59
+ <field class="o_to_single_line"
60
+ name="gross_to_single_line"
61
+ string="Single Move Line" />
62
+ </group>
63
+ <group>
64
+ <!-- Empty Column (Right) -->
65
+ </group>
66
+ </group>
67
+
68
+ <div class="oe_title">
69
+ <h2 class="oe_primary_color">
70
+ IRPF Employee
71
+ </h2>
72
+ </div>
73
+ <group>
74
+ <group>
75
+ <field name="column_irpf_employee" string="Column Number" />
76
+ <field name="irpf_employee_account_id" string="Account" />
77
+ </group>
78
+ <group>
79
+ <!-- Empty Column (Right) -->
80
+ </group>
81
+ </group>
82
+
83
+ <div class="oe_title">
84
+ <h2 class="oe_primary_color">
85
+ Net
86
+ </h2>
87
+ </div>
88
+ <group>
89
+ <group>
90
+ <field name="column_net" string="Column Number" />
91
+ <field name="net_account_id" string="Account" />
92
+ </group>
93
+ <group>
94
+ <!-- Empty Column (Right) -->
95
+ </group>
96
+ </group>
97
+
98
+ <div class="oe_title">
99
+ <h2 class="oe_primary_color">
100
+ Total TC1 o RLC
101
+ </h2>
102
+ </div>
103
+ <group>
104
+ <group>
105
+ <field name="column_total_tc1rlc" string="Column Number" />
106
+ <field name="total_tc1rlc_account_id" string="Account" />
107
+ </group>
108
+ <group>
109
+ <!-- Empty Column (Right) -->
110
+ </group>
111
+ </group>
112
+
113
+ <div class="oe_title">
114
+ <h2 class="oe_primary_color">
115
+ Employee Social Security
116
+ </h2>
117
+ </div>
118
+ <group>
119
+ <group>
120
+ <field name="column_ss_employee" string="Column Number" />
121
+ </group>
122
+ <group>
123
+ <!-- Empty Column (Right) -->
124
+ </group>
125
+ </group>
126
+
127
+ <div class="oe_title">
128
+ <h2 class="oe_primary_color">
129
+ Company Social Security
130
+ </h2>
131
+ </div>
132
+ <group>
133
+ <group>
134
+ <field name="column_ss_company" string="Column Number" />
135
+ <field name="ss_company_account_id" string="Account" />
136
+ <field class="o_to_single_line"
137
+ name="ss_company_to_single_line"
138
+ string="Single Move Line" />
139
+ </group>
140
+ <group>
141
+ <!-- Empty Column (Right) -->
142
+ </group>
143
+ </group>
144
+
145
+ <div class="oe_title">
146
+ <h2 class="oe_primary_color">
147
+ Discounts
148
+ </h2>
149
+ </div>
150
+ <group>
151
+ <group>
152
+ <field name="column_discounts" string="Column Number" />
153
+ <field name="discounts_account_id" string="Account" />
154
+ </group>
155
+ <group>
156
+ <!-- Empty Column (Right) -->
157
+ </group>
158
+ </group>
159
+
160
+ <div class="oe_title">
161
+ <h2 class="oe_primary_color">
162
+ Embargoes
163
+ </h2>
164
+ </div>
165
+ <group>
166
+ <group>
167
+ <field name="column_embargoes" string="Column Number" />
168
+ <field name="embargoes_account_id" string="Account" />
169
+ </group>
170
+ <group>
171
+ <!-- Empty Column (Right) -->
172
+ </group>
173
+ </group>
174
+
175
+ <div class="oe_title">
176
+ <h2 class="oe_primary_color">
177
+ Social Security Bonus
178
+ </h2>
179
+ </div>
180
+ <group>
181
+ <group>
182
+ <field name="column_ss_bonus" string="Column Number" />
183
+ <field name="ss_bonus_account_id" string="Account" />
184
+ </group>
185
+ <group>
186
+ <!-- Empty Column (Right) -->
187
+ </group>
188
+ </group>
189
+ <div class="oe_title">
190
+ <h1 class="oe_primary_color">
191
+ Custom Concepts
192
+ </h1>
193
+ </div>
194
+ <field
195
+ name="custom_concepts_ids"
196
+ widget="one2many_list"
197
+ nolabel="1"
198
+ >
199
+ <tree editable="bottom">
200
+ <field name="name" string="Tag" />
201
+ <field name="col_index" />
202
+ <field name="account_id" />
203
+ </tree>
204
+ </field>
205
+ <div class="oe_title">
206
+ <h1 class="oe_primary_color">
207
+ File Options (CSV)
208
+ </h1>
209
+ <p class="oe_grey oe_small_text">
210
+ Changes in this section might affect the import process
211
+ even if the file is not a CSV.
212
+ </p>
213
+ </div>
214
+ <group>
215
+ <group>
216
+ <field name="thousands_delimiter" />
217
+ <field name="decimal_delimiter" />
218
+ </group>
219
+ <group>
220
+ <field name="encoding" />
221
+ <field name="delimiter" />
222
+ </group>
223
+ </group>
224
+ </sheet>
225
+ </form>
226
+ </field>
227
+ </record>
228
+ </data>
229
+ </odoo>
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from . import payroll_import_wizard
@@ -0,0 +1,320 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import io
4
+ import chardet
5
+ import datetime
6
+ import unicodedata
7
+
8
+ from odoo import models, fields, _
9
+ from odoo.exceptions import UserError
10
+ from odoo.tools import (
11
+ pycompat,
12
+ DEFAULT_SERVER_DATE_FORMAT,
13
+ DEFAULT_SERVER_DATETIME_FORMAT,
14
+ )
15
+ from odoo.addons.base_import.models.base_import import BOM_MAP, ImportValidationError
16
+
17
+ from ..utils.file_utils import is_valid_extension, decode_file
18
+
19
+ try:
20
+ import xlrd
21
+
22
+ try:
23
+ from xlrd import xlsx
24
+ except ImportError:
25
+ xlsx = None
26
+ except ImportError:
27
+ xlrd = xlsx = None
28
+
29
+ try:
30
+ from openpyxl import load_workbook
31
+ except ImportError:
32
+ load_workbook = None
33
+
34
+
35
+ class AccountPayrollImportWizard(models.TransientModel):
36
+ _name = "payroll.import.wizard"
37
+ _description = "Account Move Payroll Import Wizard"
38
+
39
+ def _default_payroll_import_setup(self):
40
+ payroll_import_setup = self.env["payroll.import.setup"].search([], limit=1)
41
+ if not payroll_import_setup:
42
+ payroll_import_setup = self.env["payroll.import.setup"].create({})
43
+
44
+ return payroll_import_setup
45
+
46
+ file = fields.Binary(
47
+ string="File", required=True, help=_("Select the payroll file to import.")
48
+ )
49
+ file_name = fields.Char(string="File Name")
50
+ payroll_import_setup_id = fields.Many2one(
51
+ string="Import Configuration",
52
+ comodel_name="payroll.import.setup",
53
+ required=True,
54
+ default=_default_payroll_import_setup,
55
+ help=_("Select the configuration to import the payroll data."),
56
+ )
57
+ account_move_ref = fields.Char(
58
+ string="Account Move Reference",
59
+ )
60
+
61
+ def _validate_wizard(self):
62
+ """
63
+ Validate the wizard before importing the file.
64
+ - Check if the journal is set in the import configuration:
65
+ The journal is required in the UI, but not in the model definition,
66
+ so we need to check it is set.
67
+ - Check if the file extension is supported.
68
+ - Check if the 'xlrd' library is installed to read XLS or XLSX files.
69
+ :return: the file extension.
70
+ """
71
+ if not self.payroll_import_setup_id.journal_id:
72
+ raise UserError(_("Please select a journal in the import configuration."))
73
+
74
+ extension = is_valid_extension(self.file_name)
75
+ if not extension:
76
+ raise UserError(
77
+ _("Unsupported file format. Import only supports CSV, XLS and XLSX.")
78
+ )
79
+
80
+ if extension in ["xls", "xlsx"] and not xlrd and not load_workbook:
81
+ raise UserError(
82
+ _(
83
+ "Please install the 'xlrd' or 'openpyxl' library to import XLS files."
84
+ ) # noqa
85
+ )
86
+
87
+ return extension
88
+
89
+ def _read_xls(self, options):
90
+ """
91
+ Read the XLS or XLSX file content.
92
+ :param file: the file content to read.
93
+
94
+ - See also: odoo/addons/base_import/models/base_import.py
95
+ """
96
+ book = xlrd.open_workbook(file_contents=decode_file(self.file) or b"")
97
+ sheets = options["sheets"] = book.sheet_names()
98
+ sheet_name = options["sheet"] = options.get("sheet") or sheets[0]
99
+
100
+ sheet = book.sheet_by_name(sheet_name)
101
+ rows = []
102
+ # emulate Sheet.get_rows for pre-0.9.4
103
+ for rowx, row in enumerate(map(sheet.row, range(sheet.nrows)), 1):
104
+ values = []
105
+ for colx, cell in enumerate(row, 1):
106
+ if cell.ctype is xlrd.XL_CELL_NUMBER:
107
+ is_float = cell.value % 1 != 0.0
108
+ values.append(
109
+ str(cell.value) if is_float else str(int(cell.value))
110
+ )
111
+ elif cell.ctype is xlrd.XL_CELL_DATE:
112
+ is_datetime = cell.value % 1 != 0.0
113
+ # emulate xldate_as_datetime for pre-0.9.3
114
+ dt = datetime.datetime(
115
+ *xlrd.xldate.xldate_as_tuple(cell.value, book.datemode)
116
+ )
117
+ values.append(
118
+ dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
119
+ if is_datetime
120
+ else dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
121
+ )
122
+ elif cell.ctype is xlrd.XL_CELL_BOOLEAN:
123
+ values.append("True" if cell.value else "False")
124
+ elif cell.ctype is xlrd.XL_CELL_ERROR:
125
+ raise ValueError(
126
+ _(
127
+ "Invalid cell value at row %(row)s, column %(col)s: %(cell_value)s"
128
+ )
129
+ % {
130
+ "row": rowx,
131
+ "col": colx,
132
+ "cell_value": xlrd.error_text_from_code.get(
133
+ cell.value, _("unknown error code %s", cell.value)
134
+ ),
135
+ }
136
+ )
137
+ else:
138
+ values.append(cell.value)
139
+
140
+ rows.append(values)
141
+
142
+ return rows
143
+
144
+ # use the same method for xlsx and xls files
145
+ def _read_xlsx(self, options):
146
+ if xlsx:
147
+ return self._read_xls(options)
148
+
149
+ import openpyxl.cell.cell as types
150
+ import openpyxl.styles.numbers as styles # noqa: PLC0415
151
+
152
+ book = load_workbook(io.BytesIO(decode_file(self.file) or b""), data_only=True)
153
+ sheets = options["sheets"] = book.sheetnames
154
+ sheet_name = options["sheet"] = options.get("sheet") or sheets[0]
155
+ sheet = book[sheet_name]
156
+ rows = []
157
+ for rowx, row in enumerate(sheet.rows, 1):
158
+ values = []
159
+ for colx, cell in enumerate(row, 1):
160
+ if cell.data_type is types.TYPE_ERROR:
161
+ raise ValueError(
162
+ _(
163
+ "Invalid cell value at row %(row)s, column %(col)s: %(cell_value)s",
164
+ row=rowx,
165
+ col=colx,
166
+ cell_value=cell.value,
167
+ )
168
+ )
169
+
170
+ if cell.value is None:
171
+ values.append("")
172
+ elif isinstance(cell.value, float):
173
+ if cell.value % 1 == 0:
174
+ values.append(str(int(cell.value)))
175
+ else:
176
+ values.append(str(cell.value))
177
+ elif cell.is_date:
178
+ d_fmt = styles.is_datetime(cell.number_format)
179
+ if d_fmt == "datetime":
180
+ values.append(
181
+ cell.value.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
182
+ )
183
+ elif d_fmt == "date":
184
+ values.append(cell.value.strftime(DEFAULT_SERVER_DATE_FORMAT))
185
+ else:
186
+ raise ValueError(
187
+ _(
188
+ "Invalid cell format at row %(row)s, column %(col)s: %(cell_value)s, with format: %(cell_format)s, as (%(format_type)s) formats are not supported.", # noqa
189
+ row=rowx,
190
+ col=colx,
191
+ cell_value=cell.value,
192
+ cell_format=cell.number_format,
193
+ format_type=d_fmt,
194
+ )
195
+ )
196
+ else:
197
+ values.append(str(cell.value))
198
+
199
+ rows.append(values)
200
+ return rows
201
+
202
+ def _read_csv(self, options):
203
+ """Returns file length and a CSV-parsed list of all non-empty lines in the file.
204
+
205
+ :raises csv.Error: if an error is detected during CSV parsing
206
+ """
207
+ csv_data = decode_file(self.file) or b""
208
+ if not csv_data:
209
+ return []
210
+
211
+ encoding = options.get("encoding")
212
+ if not encoding:
213
+ encoding = options["encoding"] = chardet.detect(csv_data)[
214
+ "encoding"
215
+ ].lower()
216
+ # some versions of chardet (e.g. 2.3.0 but not 3.x) will return
217
+ # utf-(16|32)(le|be), which for python means "ignore / don't strip
218
+ # BOM". We don't want that, so rectify the encoding to non-marked
219
+ # IFF the guessed encoding is LE/BE and csv_data starts with a BOM
220
+ bom = BOM_MAP.get(encoding)
221
+ if bom and csv_data.startswith(bom):
222
+ encoding = options["encoding"] = encoding[:-2]
223
+
224
+ if encoding != "utf-8":
225
+ csv_data = csv_data.decode(encoding).encode("utf-8")
226
+
227
+ separator = options.get("separator")
228
+ if not separator:
229
+ # default for unspecified separator so user gets a message about
230
+ # having to specify it
231
+ separator = ","
232
+ for candidate in (
233
+ ",",
234
+ ";",
235
+ "\t",
236
+ " ",
237
+ "|",
238
+ unicodedata.lookup("unit separator"),
239
+ ):
240
+ # pass through the CSV and check if all rows are the same
241
+ # length & at least 2-wide assume it's the correct one
242
+ it = pycompat.csv_reader(
243
+ io.BytesIO(csv_data),
244
+ quotechar=options["quoting"],
245
+ delimiter=candidate,
246
+ )
247
+ w = None
248
+ for row in it:
249
+ width = len(row)
250
+ if w is None:
251
+ w = width
252
+ if width == 1 or width != w:
253
+ break # next candidate
254
+ else: # nobreak
255
+ separator = options["separator"] = candidate
256
+ break
257
+
258
+ if not len(options["quoting"]) == 1:
259
+ raise ImportValidationError(
260
+ _(
261
+ "Error while importing records: Text Delimiter should be a single character." # noqa
262
+ )
263
+ )
264
+
265
+ csv_iterator = pycompat.csv_reader(
266
+ io.BytesIO(csv_data), quotechar=options["quoting"], delimiter=separator
267
+ )
268
+
269
+ return [row for row in csv_iterator]
270
+
271
+ def _read_file(self, extension):
272
+ """
273
+ Reading based on the file extension.
274
+ """
275
+ import_setup = self.payroll_import_setup_id
276
+ options = import_setup.get_file_options()
277
+
278
+ rows, cumulative_tc1rlc_ss = [], 0.0
279
+ skip_lines = options.get("header_lines", 1) - 1
280
+ for idx, row in enumerate(getattr(self, f"_read_{extension}")(options)):
281
+ if idx == import_setup.header_ref_line - 1:
282
+ self.account_move_ref = row[0].strip() or import_setup.name
283
+
284
+ if idx < skip_lines:
285
+ continue
286
+ row = [str(col).strip() for col in row]
287
+
288
+ # exclude empty rows or rows without employee id (summary rows)
289
+ if any(row) and row[import_setup.column_employee_id - 1]:
290
+ rows.append(list(row))
291
+
292
+ # compute cumulative total tc1rlc minus ss_employee and ss_columns
293
+ cumulative_tc1rlc_ss = import_setup.compute_tc1rlc_ss_cumulative(
294
+ row, cumulative_tc1rlc_ss
295
+ )
296
+
297
+ if cumulative_tc1rlc_ss > 0.0:
298
+ raise UserError(
299
+ _(
300
+ "TC1 total should be equal to the sum of the SS Employee"
301
+ " and SS Company totals. Please check the file."
302
+ )
303
+ )
304
+
305
+ return rows
306
+
307
+ def action_import(self):
308
+ extension = self._validate_wizard()
309
+
310
+ file_content = self._read_file(extension)
311
+ if not file_content:
312
+ raise UserError(_("The file is empty."))
313
+
314
+ move = self.payroll_import_setup_id.process_data(file_content)
315
+ move.ref = self.account_move_ref
316
+
317
+ return {
318
+ "type": "ir.actions.client",
319
+ "tag": "reload",
320
+ }
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <odoo>
3
+ <data>
4
+ <record id="payroll_import_wizard_form" model="ir.ui.view">
5
+ <field name="name">Account Move Payroll Import Wizard</field>
6
+ <field name="model">payroll.import.wizard</field>
7
+ <field name="arch" type="xml">
8
+ <form string="Account Move Payroll Import Wizard">
9
+ <div class="oe_title">
10
+ <h1>
11
+ Select a payroll file to import
12
+ </h1>
13
+ <p>
14
+ Supported file formats:
15
+ <ul>
16
+ <li>CSV</li>
17
+ <li>XLS</li>
18
+ <li>XLSX</li>
19
+ </ul>
20
+ </p>
21
+ </div>
22
+ <group>
23
+ <field name="payroll_import_setup_id" string="Import Setup" required="1"/>
24
+ <field name="file_name" invisible="1"/>
25
+ <field name="file" filename="file_name" string="File" required="1"/>
26
+ </group>
27
+ <footer>
28
+ <button name="action_import" string="Import" type="object" class="btn-primary"/>
29
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
30
+ </footer>
31
+ </form>
32
+ </field>
33
+ </record>
34
+
35
+ <record id="action_payroll_import_wizard" model="ir.actions.act_window">
36
+ <field name="name">Action Payroll Import Wizard</field>
37
+ <field name="res_model">payroll.import.wizard</field>
38
+ <field name="view_mode">form</field>
39
+ <field name="target">new</field>
40
+ <field name="view_id" ref="payroll_import_wizard_form"/>
41
+ </record>
42
+
43
+ </data>
44
+ </odoo>