Merge lp:~julie-w/unifield-server/US-5658 into lp:unifield-server
- US-5658
- Merge into trunk
Proposed by
jftempo
Status: | Merged |
---|---|
Merged at revision: | 5448 |
Proposed branch: | lp:~julie-w/unifield-server/US-5658 |
Merge into: | lp:unifield-server |
Diff against target: |
278 lines (+88/-28) 3 files modified
bin/addons/msf_homere_interface/hr_payroll_wizard.xml (+11/-1) bin/addons/msf_homere_interface/wizard/hr_payroll_employee_import.py (+67/-27) bin/addons/msf_profile/i18n/fr_MF.po (+10/-0) |
To merge this branch: | bzr merge lp:~julie-w/unifield-server/US-5658 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
UniField Reviewer Team | Pending | ||
Review via email: mp+370915@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bin/addons/msf_homere_interface/hr_payroll_wizard.xml' | |||
2 | --- bin/addons/msf_homere_interface/hr_payroll_wizard.xml 2019-05-14 15:12:35 +0000 | |||
3 | +++ bin/addons/msf_homere_interface/hr_payroll_wizard.xml 2019-08-02 15:10:29 +0000 | |||
4 | @@ -108,13 +108,23 @@ | |||
5 | 108 | <field name="arch" type="xml"> | 108 | <field name="arch" type="xml"> |
6 | 109 | <form string="Import Confirmation"> | 109 | <form string="Import Confirmation"> |
7 | 110 | <field name="filename" attrs="{'invisible': [('filename', '=', False)]}"/> | 110 | <field name="filename" attrs="{'invisible': [('filename', '=', False)]}"/> |
8 | 111 | <field name="nberrors" invisible="1"/> | ||
9 | 112 | <field name="from" invisible="1"/> | ||
10 | 111 | <group colspan="4" col="8"> | 113 | <group colspan="4" col="8"> |
11 | 112 | <field name="total" attrs="{'invisible': [('state', 'in', ['none', 'payroll'])]}"/> | 114 | <field name="total" attrs="{'invisible': [('state', 'in', ['none', 'payroll'])]}"/> |
12 | 113 | <field name="created" attrs="{'invisible': [('state', '=', 'none')]}"/> | 115 | <field name="created" attrs="{'invisible': [('state', '=', 'none')]}"/> |
13 | 114 | <field name="updated" invisible="context.get('from', False) not in ['expat_employee_import', 'employee_import']"/> | 116 | <field name="updated" invisible="context.get('from', False) not in ['expat_employee_import', 'employee_import']"/> |
14 | 115 | <field name="rejected" invisible="context.get('from', False) != 'employee_import'"/> | 117 | <field name="rejected" invisible="context.get('from', False) != 'employee_import'"/> |
15 | 116 | </group> | 118 | </group> |
17 | 117 | <field name="error_line_ids" nolabel="1" colspan="4" attrs="{'invisible': [('nberrors', '=', 0)]}"> | 119 | <group colspan="4" attrs="{'invisible': ['|', ('nberrors', '=', 0), ('from', '!=', 'employee_import')]}"> |
18 | 120 | <html> | ||
19 | 121 | <p id="label_import_employee_fail" | ||
20 | 122 | style="text-align:center; color: red; font-weight: bold; font-size: 1.2em;"> | ||
21 | 123 | <translate>The import of the file failed!</translate> | ||
22 | 124 | </p> | ||
23 | 125 | </html> | ||
24 | 126 | </group> | ||
25 | 127 | <field name="error_line_ids" nolabel="1" colspan="4"> | ||
26 | 118 | <tree string="Error List"> | 128 | <tree string="Error List"> |
27 | 119 | <field name="msg"/> | 129 | <field name="msg"/> |
28 | 120 | </tree> | 130 | </tree> |
29 | 121 | 131 | ||
30 | === modified file 'bin/addons/msf_homere_interface/wizard/hr_payroll_employee_import.py' | |||
31 | --- bin/addons/msf_homere_interface/wizard/hr_payroll_employee_import.py 2019-02-15 09:33:04 +0000 | |||
32 | +++ bin/addons/msf_homere_interface/wizard/hr_payroll_employee_import.py 2019-08-02 15:10:29 +0000 | |||
33 | @@ -54,6 +54,17 @@ | |||
34 | 54 | _name = 'hr.payroll.import.confirmation' | 54 | _name = 'hr.payroll.import.confirmation' |
35 | 55 | _description = 'Import Confirmation' | 55 | _description = 'Import Confirmation' |
36 | 56 | 56 | ||
37 | 57 | def _get_from(self, cr, uid, ids, name, arg, context=None): | ||
38 | 58 | """ | ||
39 | 59 | Returns the value stored in context at index "from" = from where the wizard has been opened | ||
40 | 60 | """ | ||
41 | 61 | if context is None: | ||
42 | 62 | context = {} | ||
43 | 63 | res = {} | ||
44 | 64 | for wiz_id in ids: | ||
45 | 65 | res[wiz_id] = context.get('from') | ||
46 | 66 | return res | ||
47 | 67 | |||
48 | 57 | _columns = { | 68 | _columns = { |
49 | 58 | 'updated': fields.integer(string="Updated", size=64, readonly=True), | 69 | 'updated': fields.integer(string="Updated", size=64, readonly=True), |
50 | 59 | 'created': fields.integer(string="Created", size=64, readonly=True), | 70 | 'created': fields.integer(string="Created", size=64, readonly=True), |
51 | @@ -66,6 +77,8 @@ | |||
52 | 66 | 'errors': fields.text(string="Errors", readonly=True), | 77 | 'errors': fields.text(string="Errors", readonly=True), |
53 | 67 | 'nberrors': fields.integer(string="Errors", readonly=True), | 78 | 'nberrors': fields.integer(string="Errors", readonly=True), |
54 | 68 | 'filename': fields.char(string="Filename", size=256, readonly=True), | 79 | 'filename': fields.char(string="Filename", size=256, readonly=True), |
55 | 80 | # WARNING: this wizard model is used for the import of employees from Homere, expats, nat. staff, Payroll, HQ entries... | ||
56 | 81 | 'from': fields.function(_get_from, type='char', method=True, string="From where has this wizard been opened?", store=False), | ||
57 | 69 | } | 82 | } |
58 | 70 | 83 | ||
59 | 71 | _defaults = { | 84 | _defaults = { |
60 | @@ -188,9 +201,32 @@ | |||
61 | 188 | 'filename': fields.char(string="Imported filename", size=256), | 201 | 'filename': fields.char(string="Imported filename", size=256), |
62 | 189 | } | 202 | } |
63 | 190 | 203 | ||
64 | 204 | def store_error(self, errors, wizard_id, message): | ||
65 | 205 | """ | ||
66 | 206 | Stores the "message" in the dictionary "errors" at index "wizard_id" | ||
67 | 207 | """ | ||
68 | 208 | if errors is not None: | ||
69 | 209 | if wizard_id not in errors: | ||
70 | 210 | errors[wizard_id] = [] | ||
71 | 211 | errors[wizard_id].append(message) | ||
72 | 212 | |||
73 | 213 | def generate_errors(self, cr, uid, errors, context=None): | ||
74 | 214 | """ | ||
75 | 215 | Deletes the old errors in DB and replaces them by the new ones | ||
76 | 216 | """ | ||
77 | 217 | if context is None: | ||
78 | 218 | context = {} | ||
79 | 219 | error_obj = self.pool.get('hr.payroll.employee.import.errors') | ||
80 | 220 | error_ids = error_obj.search(cr, uid, [], order='NO_ORDER', context=context) | ||
81 | 221 | if error_ids: | ||
82 | 222 | error_obj.unlink(cr, uid, error_ids, context=context) | ||
83 | 223 | for wiz_id in errors: | ||
84 | 224 | for err in errors[wiz_id]: | ||
85 | 225 | error_obj.create(cr, uid, {'wizard_id': wiz_id, 'msg': err}, context=context) | ||
86 | 226 | |||
87 | 191 | def update_employee_check(self, cr, uid, | 227 | def update_employee_check(self, cr, uid, |
88 | 192 | staffcode=False, missioncode=False, staff_id=False, uniq_id=False, | 228 | staffcode=False, missioncode=False, staff_id=False, uniq_id=False, |
90 | 193 | wizard_id=None, employee_name=False, registered_keys=None, homere_fields=None): | 229 | wizard_id=None, employee_name=False, registered_keys=None, homere_fields=None, errors=None): |
91 | 194 | """ | 230 | """ |
92 | 195 | Check that: | 231 | Check that: |
93 | 196 | - no more than 1 employee exist for "missioncode + staff_id + uniq_id" | 232 | - no more than 1 employee exist for "missioncode + staff_id + uniq_id" |
94 | @@ -227,7 +263,7 @@ | |||
95 | 227 | message = _('No "id_staff" found for employee %s!') % (name,) | 263 | message = _('No "id_staff" found for employee %s!') % (name,) |
96 | 228 | elif not uniq_id: | 264 | elif not uniq_id: |
97 | 229 | message = _('No "id_unique" found for employee %s!') % (name,) | 265 | message = _('No "id_unique" found for employee %s!') % (name,) |
99 | 230 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wizard_id, 'msg': message}) | 266 | self.store_error(errors, wizard_id, message) |
100 | 231 | return (res, what_changed) | 267 | return (res, what_changed) |
101 | 232 | 268 | ||
102 | 233 | # Check employees | 269 | # Check employees |
103 | @@ -246,11 +282,10 @@ | |||
104 | 246 | # if not possible only the current employee name will be displayed | 282 | # if not possible only the current employee name will be displayed |
105 | 247 | else: | 283 | else: |
106 | 248 | list_duplicates = ['%s (%s)' % (employee_name, _('Import File'))] | 284 | list_duplicates = ['%s (%s)' % (employee_name, _('Import File'))] |
112 | 249 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, { | 285 | self.store_error(errors, wizard_id, |
113 | 250 | 'wizard_id': wizard_id, | 286 | _('Several employees have the same combination key codeterrain/id_staff/(id_unique) "%s / %s / (%s)": %s') % |
114 | 251 | 'msg': _('Several employees have the same combination key codeterrain/id_staff/(id_unique) "%s / %s / (%s)": %s') % | 287 | (missioncode, staff_id, uniq_id, ' ; '.join(list_duplicates)) |
115 | 252 | (missioncode, staff_id, uniq_id, ' ; '.join(list_duplicates)) | 288 | ) |
111 | 253 | }) | ||
116 | 254 | return (res, what_changed) | 289 | return (res, what_changed) |
117 | 255 | 290 | ||
118 | 256 | # check duplicates already in db | 291 | # check duplicates already in db |
119 | @@ -261,11 +296,10 @@ | |||
120 | 261 | name_duplicates = ['%s (%s)' % (employee_name, _('Import File'))] | 296 | name_duplicates = ['%s (%s)' % (employee_name, _('Import File'))] |
121 | 262 | # ... and the duplicates already in UniField | 297 | # ... and the duplicates already in UniField |
122 | 263 | name_duplicates.extend(['%s (UniField)' % emp.name for emp in emp_duplicates if emp.name]) | 298 | name_duplicates.extend(['%s (UniField)' % emp.name for emp in emp_duplicates if emp.name]) |
128 | 264 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, { | 299 | self.store_error(errors, wizard_id, |
129 | 265 | 'wizard_id': wizard_id, | 300 | _('Several employees have the same combination key codeterrain/id_staff/(id_unique) "%s / %s / (%s)": %s') % |
130 | 266 | 'msg': _('Several employees have the same combination key codeterrain/id_staff/(id_unique) "%s / %s / (%s)": %s') % | 301 | (missioncode, staff_id, uniq_id, ' ; '.join(name_duplicates)) |
131 | 267 | (missioncode, staff_id, uniq_id, ' ; '.join(name_duplicates)) | 302 | ) |
127 | 268 | }) | ||
132 | 269 | return (res, what_changed) | 303 | return (res, what_changed) |
133 | 270 | 304 | ||
134 | 271 | # Check staffcode | 305 | # Check staffcode |
135 | @@ -284,7 +318,7 @@ | |||
136 | 284 | # add the duplicated employee from Import File | 318 | # add the duplicated employee from Import File |
137 | 285 | message = _('Several employees have the same Identification No "%s": %s') % \ | 319 | message = _('Several employees have the same Identification No "%s": %s') % \ |
138 | 286 | (staffcode, ' ; '.join(["%s (%s)" % (employee_name, _('Import File'))] + employee_error_list)) | 320 | (staffcode, ' ; '.join(["%s (%s)" % (employee_name, _('Import File'))] + employee_error_list)) |
140 | 287 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wizard_id, 'msg': message}) | 321 | self.store_error(errors, wizard_id, message) |
141 | 288 | return (res, what_changed) | 322 | return (res, what_changed) |
142 | 289 | 323 | ||
143 | 290 | res = True | 324 | res = True |
144 | @@ -294,8 +328,8 @@ | |||
145 | 294 | """ | 328 | """ |
146 | 295 | Method used to check if the Identification No to be used for the employee about to be created/edited doesn't | 329 | Method used to check if the Identification No to be used for the employee about to be created/edited doesn't |
147 | 296 | already exist for another employee in UniField. | 330 | already exist for another employee in UniField. |
150 | 297 | Returns False if there is a duplication AND we are in the use case where the related and detailed | 331 | Returns False if there is a duplication AND we are in the use case where the related and detailed error has |
151 | 298 | "hr.payroll.employee.import.error" has already been created (but the process wasn't blocked earlier since "what_changed" had a value). | 332 | already been stored in the list of errors to display (but the process wasn't blocked earlier since "what_changed" had a value). |
152 | 299 | Otherwise returns True => the generic create/write checks will then apply (i.e. a generic error msg will be displayed) | 333 | Otherwise returns True => the generic create/write checks will then apply (i.e. a generic error msg will be displayed) |
153 | 300 | """ | 334 | """ |
154 | 301 | if context is None: | 335 | if context is None: |
155 | @@ -322,7 +356,7 @@ | |||
156 | 322 | return res | 356 | return res |
157 | 323 | 357 | ||
158 | 324 | def update_employee_infos(self, cr, uid, employee_data='', wizard_id=None, | 358 | def update_employee_infos(self, cr, uid, employee_data='', wizard_id=None, |
160 | 325 | line_number=None, registered_keys=None, homere_fields=None, context=None): | 359 | line_number=None, registered_keys=None, homere_fields=None, errors=None, context=None): |
161 | 326 | """ | 360 | """ |
162 | 327 | Get employee infos and set them to DB. | 361 | Get employee infos and set them to DB. |
163 | 328 | """ | 362 | """ |
164 | @@ -339,7 +373,7 @@ | |||
165 | 339 | payment_method_obj = self.pool.get('hr.payment.method') | 373 | payment_method_obj = self.pool.get('hr.payment.method') |
166 | 340 | if not employee_data or not wizard_id: | 374 | if not employee_data or not wizard_id: |
167 | 341 | message = _('No data found for this line: %s.') % line_number | 375 | message = _('No data found for this line: %s.') % line_number |
169 | 342 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wizard_id, 'msg': message}) | 376 | self.store_error(errors, wizard_id, message) |
170 | 343 | return False, created, updated | 377 | return False, created, updated |
171 | 344 | # Prepare some values | 378 | # Prepare some values |
172 | 345 | vals = {} | 379 | vals = {} |
173 | @@ -383,7 +417,8 @@ | |||
174 | 383 | staffcode=ustr(code_staff), missioncode=ustr(codeterrain), | 417 | staffcode=ustr(code_staff), missioncode=ustr(codeterrain), |
175 | 384 | staff_id=id_staff, uniq_id=ustr(uniq_id), | 418 | staff_id=id_staff, uniq_id=ustr(uniq_id), |
176 | 385 | wizard_id=wizard_id, employee_name=employee_name, | 419 | wizard_id=wizard_id, employee_name=employee_name, |
178 | 386 | registered_keys=registered_keys, homere_fields=homere_fields) | 420 | registered_keys=registered_keys, homere_fields=homere_fields, |
179 | 421 | errors=errors) | ||
180 | 387 | if not employee_check and not what_changed: | 422 | if not employee_check and not what_changed: |
181 | 388 | return False, created, updated | 423 | return False, created, updated |
182 | 389 | 424 | ||
183 | @@ -426,7 +461,7 @@ | |||
184 | 426 | payment_method_id = payment_method_ids[0] | 461 | payment_method_id = payment_method_ids[0] |
185 | 427 | else: | 462 | else: |
186 | 428 | message = _('Payment Method %s not found for line: %s. Please fix Homere configuration or request a new Payment Method to the HQ.') % (ustr(bqmodereglement), line_number) | 463 | message = _('Payment Method %s not found for line: %s. Please fix Homere configuration or request a new Payment Method to the HQ.') % (ustr(bqmodereglement), line_number) |
188 | 429 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wizard_id, 'msg': message}) | 464 | self.store_error(errors, wizard_id, message) |
189 | 430 | return False, created, updated | 465 | return False, created, updated |
190 | 431 | 466 | ||
191 | 432 | vals.update({'payment_method_id': payment_method_id}) | 467 | vals.update({'payment_method_id': payment_method_id}) |
192 | @@ -506,7 +541,7 @@ | |||
193 | 506 | registered_keys[codeterrain + id_staff + uniq_id] = True | 541 | registered_keys[codeterrain + id_staff + uniq_id] = True |
194 | 507 | else: | 542 | else: |
195 | 508 | message = _('Line %s. One of this column is missing: code_terrain, id_unique or id_staff. This often happens when the line is empty.') % (line_number) | 543 | message = _('Line %s. One of this column is missing: code_terrain, id_unique or id_staff. This often happens when the line is empty.') % (line_number) |
197 | 509 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wizard_id, 'msg': message}) | 544 | self.store_error(errors, wizard_id, message) |
198 | 510 | return False, created, updated | 545 | return False, created, updated |
199 | 511 | 546 | ||
200 | 512 | return True, created, updated | 547 | return True, created, updated |
201 | @@ -663,10 +698,7 @@ | |||
202 | 663 | processed = 0 | 698 | processed = 0 |
203 | 664 | filename = "" | 699 | filename = "" |
204 | 665 | registered_keys = {} | 700 | registered_keys = {} |
209 | 666 | # Delete old errors | 701 | errors = {} |
206 | 667 | error_ids = self.pool.get('hr.payroll.employee.import.errors').search(cr, uid, []) | ||
207 | 668 | if error_ids: | ||
208 | 669 | self.pool.get('hr.payroll.employee.import.errors').unlink(cr, uid, error_ids) | ||
210 | 670 | for wiz in self.browse(cr, uid, ids): | 702 | for wiz in self.browse(cr, uid, ids): |
211 | 671 | if not wiz.file: | 703 | if not wiz.file: |
212 | 672 | raise osv.except_osv(_('Error'), _('Nothing to import.')) | 704 | raise osv.except_osv(_('Error'), _('Nothing to import.')) |
213 | @@ -735,7 +767,7 @@ | |||
214 | 735 | for i, employee_data in enumerate(staff_seen): | 767 | for i, employee_data in enumerate(staff_seen): |
215 | 736 | update, nb_created, nb_updated = self.update_employee_infos( | 768 | update, nb_created, nb_updated = self.update_employee_infos( |
216 | 737 | cr, uid, employee_data, wiz.id, i, | 769 | cr, uid, employee_data, wiz.id, i, |
218 | 738 | registered_keys=registered_keys, homere_fields=homere_fields, context=context) | 770 | registered_keys=registered_keys, homere_fields=homere_fields, errors=errors, context=context) |
219 | 739 | if not update: | 771 | if not update: |
220 | 740 | res = False | 772 | res = False |
221 | 741 | created += nb_created | 773 | created += nb_created |
222 | @@ -746,7 +778,7 @@ | |||
223 | 746 | # create a different error line for each employee code being duplicated | 778 | # create a different error line for each employee code being duplicated |
224 | 747 | for emp_code in details: | 779 | for emp_code in details: |
225 | 748 | message = _('Several employees have the same Identification No "%s": %s') % (emp_code, ' ; '.join(details[emp_code])) | 780 | message = _('Several employees have the same Identification No "%s": %s') % (emp_code, ' ; '.join(details[emp_code])) |
227 | 749 | self.pool.get('hr.payroll.employee.import.errors').create(cr, uid, {'wizard_id': wiz.id, 'msg': message}) | 781 | self.store_error(errors, wiz.id, message) |
228 | 750 | # Close Temporary File | 782 | # Close Temporary File |
229 | 751 | # Delete previous created lines for employee's contracts | 783 | # Delete previous created lines for employee's contracts |
230 | 752 | if contract_ids: | 784 | if contract_ids: |
231 | @@ -757,9 +789,18 @@ | |||
232 | 757 | shutil.rmtree(tmpdir) | 789 | shutil.rmtree(tmpdir) |
233 | 758 | del registered_keys | 790 | del registered_keys |
234 | 759 | if res: | 791 | if res: |
235 | 792 | rejected = processed - created - updated | ||
236 | 760 | message = _("Employee import successful.") | 793 | message = _("Employee import successful.") |
237 | 761 | else: | 794 | else: |
238 | 795 | # reject the import of all employees | ||
239 | 796 | cr.rollback() | ||
240 | 797 | rejected = processed | ||
241 | 798 | created = updated = 0 | ||
242 | 762 | context.update({'employee_import_wizard_ids': ids}) | 799 | context.update({'employee_import_wizard_ids': ids}) |
243 | 800 | |||
244 | 801 | # handle the errors at the end of the process to ensure the deletion & creation aren't affected by the rollback | ||
245 | 802 | self.generate_errors(cr, uid, errors, context=context) | ||
246 | 803 | |||
247 | 763 | context.update({'message': message}) | 804 | context.update({'message': message}) |
248 | 764 | 805 | ||
249 | 765 | view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_homere_interface', 'payroll_import_confirmation') | 806 | view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_homere_interface', 'payroll_import_confirmation') |
250 | @@ -768,7 +809,6 @@ | |||
251 | 768 | # This is to redirect to Employee Tree View | 809 | # This is to redirect to Employee Tree View |
252 | 769 | context.update({'from': 'employee_import'}) | 810 | context.update({'from': 'employee_import'}) |
253 | 770 | 811 | ||
254 | 771 | rejected = processed - created - updated | ||
255 | 772 | res_id = self.pool.get('hr.payroll.import.confirmation').create(cr, uid, {'filename': filename, 'created': created, | 812 | res_id = self.pool.get('hr.payroll.import.confirmation').create(cr, uid, {'filename': filename, 'created': created, |
256 | 773 | 'updated': updated, 'total': processed, | 813 | 'updated': updated, 'total': processed, |
257 | 774 | 'rejected': rejected, 'state': 'employee'}, context) | 814 | 'rejected': rejected, 'state': 'employee'}, context) |
258 | 775 | 815 | ||
259 | === modified file 'bin/addons/msf_profile/i18n/fr_MF.po' | |||
260 | --- bin/addons/msf_profile/i18n/fr_MF.po 2019-07-31 14:57:17 +0000 | |||
261 | +++ bin/addons/msf_profile/i18n/fr_MF.po 2019-08-02 15:10:29 +0000 | |||
262 | @@ -107559,6 +107559,16 @@ | |||
263 | 107559 | msgid "Line State" | 107559 | msgid "Line State" |
264 | 107560 | msgstr "État de la Ligne" | 107560 | msgstr "État de la Ligne" |
265 | 107561 | 107561 | ||
266 | 107562 | #. module: msf_homere_interface | ||
267 | 107563 | #: view:hr.payroll.import.confirmation:0 | ||
268 | 107564 | msgid "The import of the file failed!" | ||
269 | 107565 | msgstr "L'import du fichier a échoué !" | ||
270 | 107566 | |||
271 | 107567 | #. module: msf_homere_interface | ||
272 | 107568 | #: field:hr.payroll.import.confirmation,from:0 | ||
273 | 107569 | msgid "From where has this wizard been opened?" | ||
274 | 107570 | msgstr "Depuis où cet assistant a-t-il été ouvert ?" | ||
275 | 107571 | |||
276 | 107562 | #. modules: sales_followup, msf_supply_doc_export | 107572 | #. modules: sales_followup, msf_supply_doc_export |
277 | 107563 | #: code:sales_followup/wizard/sale_followup_multi_wizard.py:190 | 107573 | #: code:sales_followup/wizard/sale_followup_multi_wizard.py:190 |
278 | 107564 | #: code:addons/msf_supply_doc_export/wizard/po_follow_up.py:297 | 107574 | #: code:addons/msf_supply_doc_export/wizard/po_follow_up.py:297 |