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.
- odoo/addons/account_move_payroll_import/CHANGELOG.md +14 -0
- odoo/addons/account_move_payroll_import/README.rst +117 -0
- odoo/addons/account_move_payroll_import/__init__.py +4 -0
- odoo/addons/account_move_payroll_import/__manifest__.py +39 -0
- odoo/addons/account_move_payroll_import/data/payroll_import_defaults.xml +108 -0
- odoo/addons/account_move_payroll_import/i18n/ca_ES.po +828 -0
- odoo/addons/account_move_payroll_import/i18n/es.po +829 -0
- odoo/addons/account_move_payroll_import/models/__init__.py +6 -0
- odoo/addons/account_move_payroll_import/models/account_move.py +167 -0
- odoo/addons/account_move_payroll_import/models/payroll_custom_concept.py +57 -0
- odoo/addons/account_move_payroll_import/models/payroll_import_mapping.py +28 -0
- odoo/addons/account_move_payroll_import/models/payroll_import_setup.py +497 -0
- odoo/addons/account_move_payroll_import/security/ir.model.access.csv +5 -0
- odoo/addons/account_move_payroll_import/static/src/css/styles.css +26 -0
- odoo/addons/account_move_payroll_import/static/src/js/payroll_import_button.js +32 -0
- odoo/addons/account_move_payroll_import/static/src/xml/payroll_import_templates.xml +24 -0
- odoo/addons/account_move_payroll_import/utils/__init__.py +0 -0
- odoo/addons/account_move_payroll_import/utils/file_utils.py +31 -0
- odoo/addons/account_move_payroll_import/utils/parse_utils.py +34 -0
- odoo/addons/account_move_payroll_import/views/assets_template.xml +13 -0
- odoo/addons/account_move_payroll_import/views/payroll_import_views.xml +229 -0
- odoo/addons/account_move_payroll_import/wizards/__init__.py +3 -0
- odoo/addons/account_move_payroll_import/wizards/payroll_import_wizard.py +320 -0
- odoo/addons/account_move_payroll_import/wizards/payroll_import_wizard.xml +44 -0
- odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/METADATA +131 -0
- odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/RECORD +28 -0
- odoo_addon_account_move_payroll_import-16.0.1.0.0.dist-info/WHEEL +6 -0
- 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,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>
|