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,497 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from odoo import models, fields, _, Command
|
|
4
|
+
from odoo.exceptions import UserError
|
|
5
|
+
|
|
6
|
+
from ..utils.parse_utils import abs_float
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# This are the only allowed mappings for the moment
|
|
10
|
+
ALLOWED_MAPPINGS = [
|
|
11
|
+
"payroll_import_mapping_gross",
|
|
12
|
+
"payroll_import_mapping_net",
|
|
13
|
+
"payroll_import_mapping_total_tc1rlc",
|
|
14
|
+
"payroll_import_mapping_irpf_employee",
|
|
15
|
+
"payroll_import_mapping_ss_company",
|
|
16
|
+
"payroll_import_mapping_discounts",
|
|
17
|
+
"payroll_import_mapping_embargoes",
|
|
18
|
+
"payroll_import_mapping_ss_bonus",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PayrollImportSetup(models.Model):
|
|
23
|
+
_name = "payroll.import.setup"
|
|
24
|
+
_description = "Payroll Import Setup"
|
|
25
|
+
_check_company_auto = True
|
|
26
|
+
|
|
27
|
+
NUMBER_DELIMITERS = [
|
|
28
|
+
(".", _("Dot (.)")),
|
|
29
|
+
(",", _("Comma (,)")),
|
|
30
|
+
("", _("None")),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
ALL_DELIMITERS = [
|
|
34
|
+
(".", _("Dot (.)")),
|
|
35
|
+
(",", _("Comma (,)")),
|
|
36
|
+
(";", _("Semicolon (;)")),
|
|
37
|
+
("\t", _("Tabulation")),
|
|
38
|
+
(" ", _("Space")),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
ENCODINGS = [
|
|
42
|
+
("utf-8", "UTF-8"),
|
|
43
|
+
("latin1", "Latin1"),
|
|
44
|
+
("ascii", "ASCII"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def _default_import_mappings(self):
|
|
48
|
+
"""
|
|
49
|
+
This version only allows the default mappings in payroll_import_defaults.xml
|
|
50
|
+
"""
|
|
51
|
+
return [
|
|
52
|
+
Command.link(self.env.ref("account_move_payroll_import.%s" % xml_id).id)
|
|
53
|
+
for xml_id in ALLOWED_MAPPINGS
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def _default_custom_concepts(self):
|
|
57
|
+
return [
|
|
58
|
+
Command.create(
|
|
59
|
+
{
|
|
60
|
+
"name": "FUNDAE",
|
|
61
|
+
"col_index": 13,
|
|
62
|
+
"default_account_xml_id": "account_common_642",
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
Command.create(
|
|
66
|
+
{
|
|
67
|
+
"name": "AN TANDEM",
|
|
68
|
+
"col_index": 15,
|
|
69
|
+
"default_account_xml_id": "account_common_640",
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
Command.create(
|
|
73
|
+
{
|
|
74
|
+
"name": "RET FLEXIB",
|
|
75
|
+
"col_index": 16,
|
|
76
|
+
"default_account_xml_id": "account_common_642",
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
name = fields.Char(
|
|
82
|
+
string="Name",
|
|
83
|
+
default=_("Payroll Import A3 2024"),
|
|
84
|
+
help=_("Indicate a name describing the payroll file."),
|
|
85
|
+
)
|
|
86
|
+
thousands_delimiter = fields.Selection(
|
|
87
|
+
selection=NUMBER_DELIMITERS, string="Thousands Separator", default=","
|
|
88
|
+
)
|
|
89
|
+
decimal_delimiter = fields.Selection(
|
|
90
|
+
selection=NUMBER_DELIMITERS, string="Decimal Separator", default="."
|
|
91
|
+
)
|
|
92
|
+
encoding = fields.Selection(selection=ENCODINGS, string="Encoding", default="utf-8")
|
|
93
|
+
delimiter = fields.Selection(
|
|
94
|
+
selection=ALL_DELIMITERS, string="Column Separator", default=";"
|
|
95
|
+
)
|
|
96
|
+
header_lines = fields.Integer(
|
|
97
|
+
string="Header Lines",
|
|
98
|
+
default=9,
|
|
99
|
+
help=_("Specify the number of header lines in the file."),
|
|
100
|
+
)
|
|
101
|
+
header_ref_line = fields.Integer(
|
|
102
|
+
string="Header Reference (Tag) Line",
|
|
103
|
+
default=4,
|
|
104
|
+
help=_("Specify in case of a header line with the account move reference."),
|
|
105
|
+
)
|
|
106
|
+
journal_id = fields.Many2one(
|
|
107
|
+
string="Journal",
|
|
108
|
+
comodel_name="account.journal",
|
|
109
|
+
help=_("Indicate the journal where data will be posted."),
|
|
110
|
+
)
|
|
111
|
+
column_employee_id = fields.Integer(
|
|
112
|
+
string="Employee ID Column Number",
|
|
113
|
+
required=True,
|
|
114
|
+
default=2,
|
|
115
|
+
help=_("Indicate the column number with employee reference."),
|
|
116
|
+
)
|
|
117
|
+
column_gross = fields.Integer(
|
|
118
|
+
string="Gross Salary Column Number",
|
|
119
|
+
required=True,
|
|
120
|
+
default=4,
|
|
121
|
+
help=_("Indicate the column number with gross salary in the file."),
|
|
122
|
+
)
|
|
123
|
+
gross_account_id = fields.Many2one(
|
|
124
|
+
string="Gross Salary Account",
|
|
125
|
+
comodel_name="account.account",
|
|
126
|
+
required=True,
|
|
127
|
+
default=lambda self: self.env.ref(
|
|
128
|
+
"l10n_es.%s_account_common_640" % self.env.company.id,
|
|
129
|
+
raise_if_not_found=False,
|
|
130
|
+
),
|
|
131
|
+
help=_("Indicate the account where data will be posted."),
|
|
132
|
+
)
|
|
133
|
+
gross_tax_id = fields.Many2one(
|
|
134
|
+
string="Gross Salary Tax",
|
|
135
|
+
comodel_name="account.tax",
|
|
136
|
+
required=True,
|
|
137
|
+
default=lambda self: self.env.ref(
|
|
138
|
+
"l10n_es.%s_account_tax_template_p_irpf21td" % self.env.company.id,
|
|
139
|
+
raise_if_not_found=False,
|
|
140
|
+
),
|
|
141
|
+
help=_("Indicate the tax to be applied to the data."),
|
|
142
|
+
)
|
|
143
|
+
gross_to_single_line = fields.Boolean(
|
|
144
|
+
string="Gross Single Move Line",
|
|
145
|
+
default=True,
|
|
146
|
+
help=_("Aggregate amounts in a single move line if True."),
|
|
147
|
+
)
|
|
148
|
+
column_net = fields.Integer(
|
|
149
|
+
string="Net Salary Column Number",
|
|
150
|
+
required=True,
|
|
151
|
+
default=5,
|
|
152
|
+
help=_("Indicate the column number with net salary in the file."),
|
|
153
|
+
)
|
|
154
|
+
net_account_id = fields.Many2one(
|
|
155
|
+
string="Net Salary Account",
|
|
156
|
+
comodel_name="account.account",
|
|
157
|
+
required=True,
|
|
158
|
+
default=lambda self: self.env.ref(
|
|
159
|
+
"l10n_es.%s_account_common_465" % self.env.company.id,
|
|
160
|
+
raise_if_not_found=False,
|
|
161
|
+
),
|
|
162
|
+
help=_("Indicate the account where data will be posted."),
|
|
163
|
+
)
|
|
164
|
+
column_total_tc1rlc = fields.Integer(
|
|
165
|
+
string="Total TC1RLC Column Number",
|
|
166
|
+
required=True,
|
|
167
|
+
default=7,
|
|
168
|
+
help=_("SS quotes from all employees."),
|
|
169
|
+
)
|
|
170
|
+
total_tc1rlc_account_id = fields.Many2one(
|
|
171
|
+
string="Total TC1RLC Account",
|
|
172
|
+
comodel_name="account.account",
|
|
173
|
+
required=True,
|
|
174
|
+
default=lambda self: self.env.ref(
|
|
175
|
+
"l10n_es.%s_account_common_476" % self.env.company.id,
|
|
176
|
+
raise_if_not_found=False,
|
|
177
|
+
),
|
|
178
|
+
help=_("Indicate the account where data will be posted."),
|
|
179
|
+
)
|
|
180
|
+
total_tc1rlc_to_single_line = fields.Boolean(
|
|
181
|
+
string="Total TC1RLC Single Move Line",
|
|
182
|
+
default=True,
|
|
183
|
+
help=_("Aggregate amounts in a single move line if True."),
|
|
184
|
+
)
|
|
185
|
+
column_irpf_employee = fields.Integer(
|
|
186
|
+
string="IRPF Employee Column Number",
|
|
187
|
+
required=True,
|
|
188
|
+
default=8,
|
|
189
|
+
help=_("Indicate the column number with IRPF employee in the file."),
|
|
190
|
+
)
|
|
191
|
+
irpf_employee_account_id = fields.Many2one(
|
|
192
|
+
string="IRPF Employee Account",
|
|
193
|
+
comodel_name="account.account",
|
|
194
|
+
required=True,
|
|
195
|
+
default=lambda self: self.env.ref(
|
|
196
|
+
"l10n_es.%s_account_common_4751" % self.env.company.id,
|
|
197
|
+
raise_if_not_found=False,
|
|
198
|
+
),
|
|
199
|
+
help=_("Indicate the account where data will be posted."),
|
|
200
|
+
)
|
|
201
|
+
irpf_employee_to_single_line = fields.Boolean(
|
|
202
|
+
string="IRPF Single Move Line",
|
|
203
|
+
default=True,
|
|
204
|
+
help=_("Aggregate amounts in a single move line if True."),
|
|
205
|
+
)
|
|
206
|
+
column_ss_employee = fields.Integer(
|
|
207
|
+
string="SS Employee Column Number",
|
|
208
|
+
required=True,
|
|
209
|
+
default=9,
|
|
210
|
+
help=_(
|
|
211
|
+
"Specify the position in the file."
|
|
212
|
+
" This column is used to verify total values of TC1 - SS company."
|
|
213
|
+
" It won't be registered in the account move."
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
column_ss_company = fields.Integer(
|
|
217
|
+
string="SS Company Column Number",
|
|
218
|
+
required=True,
|
|
219
|
+
default=10,
|
|
220
|
+
help=_("Specify the position in the file."),
|
|
221
|
+
)
|
|
222
|
+
ss_company_account_id = fields.Many2one(
|
|
223
|
+
string="SS Company Account",
|
|
224
|
+
comodel_name="account.account",
|
|
225
|
+
required=True,
|
|
226
|
+
default=lambda self: self.env.ref(
|
|
227
|
+
"l10n_es.%s_account_common_642" % self.env.company.id,
|
|
228
|
+
raise_if_not_found=False,
|
|
229
|
+
),
|
|
230
|
+
help=_("Indicate the account where data will be posted."),
|
|
231
|
+
)
|
|
232
|
+
ss_company_to_single_line = fields.Boolean(
|
|
233
|
+
string="SS Company Single Move Line",
|
|
234
|
+
default=True,
|
|
235
|
+
help=_("Aggregate amounts in a single move line if True."),
|
|
236
|
+
)
|
|
237
|
+
column_discounts = fields.Integer(
|
|
238
|
+
string="Discounts Column Number",
|
|
239
|
+
required=True,
|
|
240
|
+
default=11,
|
|
241
|
+
help=_("Indicate the column number with discounts in the file."),
|
|
242
|
+
)
|
|
243
|
+
discounts_account_id = fields.Many2one(
|
|
244
|
+
string="Discounts Account",
|
|
245
|
+
comodel_name="account.account",
|
|
246
|
+
required=True,
|
|
247
|
+
default=lambda self: self.env.ref(
|
|
248
|
+
"l10n_es.%s_account_common_460" % self.env.company.id,
|
|
249
|
+
raise_if_not_found=False,
|
|
250
|
+
),
|
|
251
|
+
help=_("Indicate the account where data will be posted."),
|
|
252
|
+
)
|
|
253
|
+
column_embargoes = fields.Integer(
|
|
254
|
+
string="Embargoes Column Number",
|
|
255
|
+
required=True,
|
|
256
|
+
default=12,
|
|
257
|
+
help=_("Indicate the column number with embargoes in the file."),
|
|
258
|
+
)
|
|
259
|
+
embargoes_account_id = fields.Many2one(
|
|
260
|
+
string="Embargoes Account",
|
|
261
|
+
comodel_name="account.account",
|
|
262
|
+
required=True,
|
|
263
|
+
default=lambda self: self.env.ref(
|
|
264
|
+
"l10n_es.%s_account_common_465" % self.env.company.id,
|
|
265
|
+
raise_if_not_found=False,
|
|
266
|
+
),
|
|
267
|
+
help=_("Indicate the account where data will be posted."),
|
|
268
|
+
)
|
|
269
|
+
column_ss_bonus = fields.Integer(
|
|
270
|
+
string="SS Bonus Column Number",
|
|
271
|
+
required=True,
|
|
272
|
+
default=14,
|
|
273
|
+
help=_("Indicate the column number with SS bonus in the file."),
|
|
274
|
+
)
|
|
275
|
+
ss_bonus_account_id = fields.Many2one(
|
|
276
|
+
string="SS Bonus Account",
|
|
277
|
+
comodel_name="account.account",
|
|
278
|
+
required=True,
|
|
279
|
+
default=lambda self: self.env.ref(
|
|
280
|
+
"l10n_es.%s_account_common_642" % self.env.company.id,
|
|
281
|
+
raise_if_not_found=False,
|
|
282
|
+
),
|
|
283
|
+
help=_("Indicate the account where data will be posted."),
|
|
284
|
+
)
|
|
285
|
+
custom_concepts_ids = fields.One2many(
|
|
286
|
+
string="Custom Concepts",
|
|
287
|
+
comodel_name="payroll.custom.concept",
|
|
288
|
+
inverse_name="payroll_import_setup_id",
|
|
289
|
+
default=_default_custom_concepts,
|
|
290
|
+
)
|
|
291
|
+
payroll_import_mapping_ids = fields.Many2many(
|
|
292
|
+
string="Payroll Import Mapping",
|
|
293
|
+
comodel_name="payroll.import.mapping",
|
|
294
|
+
default=_default_import_mappings,
|
|
295
|
+
)
|
|
296
|
+
company_id = fields.Many2one(
|
|
297
|
+
string="Company",
|
|
298
|
+
comodel_name="res.company",
|
|
299
|
+
default=lambda self: self.env.company,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
_sql_constraints = [
|
|
303
|
+
(
|
|
304
|
+
"name_company_uniq",
|
|
305
|
+
"unique(name, company_id)",
|
|
306
|
+
_("The name must be unique by company."),
|
|
307
|
+
)
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
def get_file_options(self):
|
|
311
|
+
"""
|
|
312
|
+
Get file options in a dictionary formatted as required by
|
|
313
|
+
the base_import.import module method _read_file.
|
|
314
|
+
"""
|
|
315
|
+
return {
|
|
316
|
+
"encoding": self.encoding,
|
|
317
|
+
"separator": self.delimiter,
|
|
318
|
+
"float_thousand_separator": self.thousands_delimiter,
|
|
319
|
+
"float_decimal_separator": self.decimal_delimiter,
|
|
320
|
+
"header_lines": self.header_lines,
|
|
321
|
+
"headers": False,
|
|
322
|
+
"quoting": '"',
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
def validate_column_numbers(self, data_length):
|
|
326
|
+
for field in [k for k in self._fields.keys() if k.startswith("column_")]:
|
|
327
|
+
if getattr(self, field) > data_length:
|
|
328
|
+
raise UserError(_("Column %s is out of range." % field))
|
|
329
|
+
|
|
330
|
+
def compute_tc1rlc_ss_cumulative(self, row, cumulative):
|
|
331
|
+
"""
|
|
332
|
+
Calculate the cumulative of TC1RLC - (SS employee + SS company).
|
|
333
|
+
It should be zero once all rows are processed.
|
|
334
|
+
"""
|
|
335
|
+
cumulative += (
|
|
336
|
+
abs_float(
|
|
337
|
+
row[self.column_total_tc1rlc - 1],
|
|
338
|
+
self.thousands_delimiter,
|
|
339
|
+
self.decimal_delimiter,
|
|
340
|
+
)
|
|
341
|
+
- abs_float(
|
|
342
|
+
row[self.column_ss_employee - 1],
|
|
343
|
+
self.thousands_delimiter,
|
|
344
|
+
self.decimal_delimiter,
|
|
345
|
+
)
|
|
346
|
+
- abs_float(
|
|
347
|
+
row[self.column_ss_company - 1],
|
|
348
|
+
self.thousands_delimiter,
|
|
349
|
+
self.decimal_delimiter,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return cumulative
|
|
353
|
+
|
|
354
|
+
def _get_partner_id(self, employee_id):
|
|
355
|
+
"""
|
|
356
|
+
Get the partner id from the employee id.
|
|
357
|
+
"""
|
|
358
|
+
employee = self.env["hr.employee"].search(
|
|
359
|
+
[("identification_id", "=", employee_id)], limit=1
|
|
360
|
+
)
|
|
361
|
+
if not employee:
|
|
362
|
+
raise UserError(
|
|
363
|
+
_("Employee with identification id %s not found." % employee_id)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
partner = employee.address_home_id
|
|
367
|
+
if not partner:
|
|
368
|
+
raise UserError(
|
|
369
|
+
_("Employee %s has no home address (partner)." % employee.name)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return partner.id
|
|
373
|
+
|
|
374
|
+
def _prepare_line_vals(self, row, line_mapping):
|
|
375
|
+
"""
|
|
376
|
+
Prepare the line values for the account move line.
|
|
377
|
+
"""
|
|
378
|
+
if not line_mapping.res_field: # skip validation columns
|
|
379
|
+
return {}
|
|
380
|
+
|
|
381
|
+
column = getattr(self, line_mapping.column_field, False)
|
|
382
|
+
value = abs_float(
|
|
383
|
+
row[column - 1], self.thousands_delimiter, self.decimal_delimiter
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if not value:
|
|
387
|
+
return {} # skip lines with no amounts
|
|
388
|
+
|
|
389
|
+
vals = {"tax_ids": False, "account_id": False, line_mapping.res_field: value}
|
|
390
|
+
|
|
391
|
+
if line_mapping.tax_field and not vals.get("tax_ids"):
|
|
392
|
+
tax_id = getattr(self, line_mapping.tax_field, False)
|
|
393
|
+
vals.update({"tax_ids": [(6, 0, [tax_id.id])] if tax_id else False})
|
|
394
|
+
|
|
395
|
+
if line_mapping.account_field and not vals.get("account_id"):
|
|
396
|
+
account_id = getattr(self, line_mapping.account_field, False)
|
|
397
|
+
vals.update({"account_id": account_id.id if account_id else False})
|
|
398
|
+
|
|
399
|
+
if (
|
|
400
|
+
line_mapping.column_field == "column_gross"
|
|
401
|
+
and self.gross_to_single_line
|
|
402
|
+
and not vals.get("tax_tag_ids")
|
|
403
|
+
):
|
|
404
|
+
tax_tag_ids = self.gross_tax_id.get_tax_tags(
|
|
405
|
+
is_refund=False, repartition_type="base"
|
|
406
|
+
).ids
|
|
407
|
+
vals.update({"tax_tag_ids": [(6, 0, tax_tag_ids)]})
|
|
408
|
+
|
|
409
|
+
if (
|
|
410
|
+
line_mapping.column_field == "column_irpf_employee"
|
|
411
|
+
and self.irpf_employee_to_single_line
|
|
412
|
+
and not vals.get("tax_line_id")
|
|
413
|
+
):
|
|
414
|
+
tax_tag_ids = self.gross_tax_id.get_tax_tags(
|
|
415
|
+
is_refund=False, repartition_type="tax"
|
|
416
|
+
).ids
|
|
417
|
+
vals.update(
|
|
418
|
+
{
|
|
419
|
+
"name": self.gross_tax_id.name,
|
|
420
|
+
"tax_line_id": self.gross_tax_id.id,
|
|
421
|
+
"display_type": "tax",
|
|
422
|
+
"tax_tag_ids": [(6, 0, tax_tag_ids)],
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
return vals
|
|
426
|
+
|
|
427
|
+
def _prepare_custom_concept_vals_list(self, row, partner_id):
|
|
428
|
+
"""
|
|
429
|
+
Prepare the line values for the account move lines related
|
|
430
|
+
to custom concepts.
|
|
431
|
+
"""
|
|
432
|
+
cc_vals_list = []
|
|
433
|
+
for custom_concept in self.custom_concepts_ids:
|
|
434
|
+
column = custom_concept.col_index
|
|
435
|
+
value = abs_float(
|
|
436
|
+
row[column - 1], self.thousands_delimiter, self.decimal_delimiter
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if not value:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
cc_vals_list.append(
|
|
443
|
+
{
|
|
444
|
+
"partner_id": partner_id,
|
|
445
|
+
"account_id": custom_concept.account_id.id,
|
|
446
|
+
"name": custom_concept.name,
|
|
447
|
+
"credit": value,
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return cc_vals_list
|
|
452
|
+
|
|
453
|
+
def _create_account_move(self, lines_vals):
|
|
454
|
+
"""
|
|
455
|
+
Create the payroll account move.
|
|
456
|
+
"""
|
|
457
|
+
move = self.env["account.move"].with_context(
|
|
458
|
+
is_payroll_import=True
|
|
459
|
+
).create(
|
|
460
|
+
{
|
|
461
|
+
"date": fields.Date.today(),
|
|
462
|
+
"journal_id": self.journal_id.id,
|
|
463
|
+
"line_ids": [(0, 0, vals) for vals in lines_vals],
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
return move
|
|
467
|
+
|
|
468
|
+
def process_data(self, data):
|
|
469
|
+
"""
|
|
470
|
+
Process the data from the file and create the account move.
|
|
471
|
+
"""
|
|
472
|
+
line_mappings = self.payroll_import_mapping_ids.filtered(
|
|
473
|
+
lambda x: x.res_model == "account.move.line"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
vals_list, agg_lines = [], {}
|
|
477
|
+
for row in data:
|
|
478
|
+
partner_id = self._get_partner_id(row[self.column_employee_id - 1])
|
|
479
|
+
for mapping in line_mappings:
|
|
480
|
+
vals = self._prepare_line_vals(row, mapping)
|
|
481
|
+
if not vals:
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
if mapping.aggregate_field and getattr(self, mapping.aggregate_field):
|
|
485
|
+
current_amount = vals.get(mapping.res_field, 0.0)
|
|
486
|
+
if mapping.id in agg_lines:
|
|
487
|
+
agg_lines[mapping.id][mapping.res_field] += current_amount
|
|
488
|
+
else:
|
|
489
|
+
agg_lines[mapping.id] = vals
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
vals_list.append({**vals, "partner_id": partner_id})
|
|
493
|
+
|
|
494
|
+
vals_list.extend(self._prepare_custom_concept_vals_list(row, partner_id))
|
|
495
|
+
|
|
496
|
+
vals_list.extend(agg_lines.values())
|
|
497
|
+
return self._create_account_move(vals_list)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|
2
|
+
access_payroll_import_wizard,payroll.import.wizard,model_payroll_import_wizard,account.group_account_user,1,1,1,1
|
|
3
|
+
access_payroll_import_setup,payroll.import.setup,model_payroll_import_setup,account.group_account_user,1,1,1,1
|
|
4
|
+
access_payroll_custom_concept,payroll.custom.concept,model_payroll_custom_concept,account.group_account_user,1,1,1,1
|
|
5
|
+
access_payroll_import_mapping,payroll.import.mapping,model_payroll_import_mapping,account.group_account_user,1,1,1,1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.oe_primary_color {
|
|
2
|
+
color: #7C7BAD !important;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.oe_small_text {
|
|
6
|
+
font-size: 1.2rem;
|
|
7
|
+
line-height: normal !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.o_to_single_line {
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
text-overflow: ellipsis;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.o_to_single_line > :first-child::after {
|
|
17
|
+
content: 'One line per employee if unchecked.';
|
|
18
|
+
display: inline-block;
|
|
19
|
+
width: 100%;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
text-overflow: ellipsis;
|
|
23
|
+
font-size: .94rem;
|
|
24
|
+
color: #7C7BAD !important;
|
|
25
|
+
margin-top: 2px;
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @odoo-module */
|
|
2
|
+
|
|
3
|
+
import { registry } from '@web/core/registry';
|
|
4
|
+
import { listView } from '@web/views/list/list_view';
|
|
5
|
+
import { ListController } from "@web/views/list/list_controller";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const ResModel = 'payroll.import.wizard';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export class AccountMovePayrollImportController extends ListController {
|
|
12
|
+
setup() {
|
|
13
|
+
super.setup();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
OnPayrollImportClick() {
|
|
17
|
+
this.actionService.doAction({
|
|
18
|
+
type: 'ir.actions.act_window',
|
|
19
|
+
name: this.env._t('Import Payroll Wizard'),
|
|
20
|
+
res_model: ResModel,
|
|
21
|
+
views: [[false, 'form']],
|
|
22
|
+
target: 'new',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
registry.category("views").add("payroll_import_button_in_tree", {
|
|
29
|
+
...listView,
|
|
30
|
+
Controller: AccountMovePayrollImportController,
|
|
31
|
+
buttonTemplate: "account_move_payroll_import.ListView.Buttons",
|
|
32
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<templates xml:space="preserve">
|
|
3
|
+
<t t-name="account_move_payroll_import.PayrollImportButton" owl="1">
|
|
4
|
+
<button
|
|
5
|
+
class="btn btn-secondary o_button_import_payroll"
|
|
6
|
+
t-on-click="OnPayrollImportClick"
|
|
7
|
+
>
|
|
8
|
+
Payroll Import
|
|
9
|
+
</button>
|
|
10
|
+
</t>
|
|
11
|
+
<t
|
|
12
|
+
t-name="account_move_payroll_import.ListView.Buttons"
|
|
13
|
+
t-inherit="web.ListView.Buttons"
|
|
14
|
+
t-inherit-mode="primary"
|
|
15
|
+
owl="1"
|
|
16
|
+
>
|
|
17
|
+
<xpath
|
|
18
|
+
expr="//*[@class='btn btn-primary o_list_button_add']"
|
|
19
|
+
position="after"
|
|
20
|
+
>
|
|
21
|
+
<t t-call="account_move_payroll_import.PayrollImportButton"/>
|
|
22
|
+
</xpath>
|
|
23
|
+
</t>
|
|
24
|
+
</templates>
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import base64
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
EXTENSIONS = [".csv", ".xls", ".xlsx"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_file_extension(file_name):
|
|
9
|
+
"""
|
|
10
|
+
Get the file extension from the file name.
|
|
11
|
+
"""
|
|
12
|
+
if file_name:
|
|
13
|
+
return os.path.splitext(file_name)[-1]
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def decode_file(file_contents):
|
|
18
|
+
"""
|
|
19
|
+
Decode the file content.
|
|
20
|
+
"""
|
|
21
|
+
return base64.b64decode(file_contents)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_valid_extension(file_name):
|
|
25
|
+
"""
|
|
26
|
+
Get the valid file extension.
|
|
27
|
+
"""
|
|
28
|
+
ext = get_file_extension(file_name)
|
|
29
|
+
if ext not in EXTENSIONS:
|
|
30
|
+
return False
|
|
31
|
+
return ext[1:]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_float(value, thousands_sep=",", decimal_sep="."):
|
|
5
|
+
"""
|
|
6
|
+
Parse a float value.
|
|
7
|
+
:param thousands_sep: the thousands separator.
|
|
8
|
+
:param decimal_sep: the decimal separator.
|
|
9
|
+
:param value: the value to parse.
|
|
10
|
+
:return: the parsed float value.
|
|
11
|
+
"""
|
|
12
|
+
if type(value) in (int, float, bool):
|
|
13
|
+
return float(value)
|
|
14
|
+
|
|
15
|
+
value = str(value).strip()
|
|
16
|
+
if not value:
|
|
17
|
+
return 0.0
|
|
18
|
+
|
|
19
|
+
value = value.replace(thousands_sep, "").replace(decimal_sep, ".")
|
|
20
|
+
return float(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def abs_float(value, thousands_sep=",", decimal_sep="."):
|
|
24
|
+
"""
|
|
25
|
+
Parse a float value and return its absolute value.
|
|
26
|
+
:param thousands_sep: the thousands separator.
|
|
27
|
+
:param decimal_sep: the decimal separator.
|
|
28
|
+
:param value: the value to parse.
|
|
29
|
+
:return: the absolute value of the parsed float value.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
return abs(parse_float(value, thousands_sep, decimal_sep))
|
|
33
|
+
except ValueError:
|
|
34
|
+
raise ValueError("Invalid float value: %s" % value)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
2
|
+
<odoo>
|
|
3
|
+
<record id="account_view_move_tree_inherit_js_class" model="ir.ui.view">
|
|
4
|
+
<field name="name">account.move.tree.payroll.inherit</field>
|
|
5
|
+
<field name="model">account.move</field>
|
|
6
|
+
<field name="inherit_id" ref="account.view_move_tree"/>
|
|
7
|
+
<field name="arch" type="xml">
|
|
8
|
+
<xpath expr="//tree" position="attributes">
|
|
9
|
+
<attribute name="js_class">payroll_import_button_in_tree</attribute>
|
|
10
|
+
</xpath>
|
|
11
|
+
</field>
|
|
12
|
+
</record>
|
|
13
|
+
</odoo>
|