Commit | Line | Data |
---|---|---|
b56e39ea P |
1 | #! |
2 | # -*- coding: utf-8 -*- | |
4928a69a P |
3 | """ |
4 | Extension pour faciliter l'intégration du logiciel Coda. | |
5 | Macro pour faciliter l'intégration des données provenant de Coda dans | |
6 | une feuille de calcul. | |
7 | ||
8 | Copyright : Agence universitaire de la Francophonie | |
9 | Licence : GNU General Public Licence, version 2 | |
10 | Auteur : Jean Christophe André | |
11 | Date de création : septembre 2009 | |
12 | """ | |
b56e39ea | 13 | import re |
b8c5c6f9 | 14 | import datetime |
b56e39ea P |
15 | import uno |
16 | import unohelper | |
17 | from com.sun.star.awt import WindowDescriptor | |
18 | from com.sun.star.awt.WindowClass import MODALTOP | |
19 | from com.sun.star.awt.VclWindowPeerAttribute import OK, DEF_OK | |
20 | from com.sun.star.uno import Exception as UnoException, RuntimeException | |
21 | from com.sun.star.connection import NoConnectException | |
22 | from com.sun.star.lang import Locale, IllegalArgumentException | |
23 | from com.sun.star.beans import PropertyValue | |
24 | #from com.sun.star.util.ParagraphProperties import CENTER | |
b8c5c6f9 P |
25 | from com.sun.star.util.NumberFormat import CURRENCY, DATE, NUMBER |
26 | from com.sun.star.i18n.NumberFormatIndex import NUMBER_1000DEC2 | |
b56e39ea P |
27 | from com.sun.star.container import NoSuchElementException |
28 | ||
4928a69a P |
29 | COLONNES_NUMERIQUES = ('Montant doc','Montant EUR') |
30 | ||
b56e39ea P |
31 | ############################################################################## |
32 | ||
33 | def clipboard_data(ctx, mimetype='text/plain;charset=utf-16'): | |
34 | clipboard = ctx.ServiceManager.createInstanceWithContext( | |
35 | "com.sun.star.datatransfer.clipboard.SystemClipboard", ctx) | |
36 | contents = clipboard.getContents() | |
37 | #print "Contents:\n%s\n%s\n%s" % ("-" * 78, "* " + "\n* ".join(dir(contents)), "=" * 78) | |
38 | #flavors = contents.getTransferDataFlavors() | |
39 | #print "Flavors:\n%s\n%s\n%s" % ("-" * 78, "* " + "\n* ".join([flavor.MimeType for flavor in flavors]), "=" * 78) | |
40 | found_flavor = None | |
41 | for flavor in contents.getTransferDataFlavors(): | |
42 | if flavor.MimeType == mimetype: | |
43 | found_flavor = flavor | |
44 | break | |
45 | if not found_flavor: | |
46 | raise RuntimeError, u"Erreur : type de données '%s' non disponible.\n" \ | |
47 | u"\nAvez-vous bien sélectionné puis copié les données dans CODA ?" \ | |
48 | % mimetype | |
49 | data = contents.getTransferData(found_flavor) | |
50 | #print "Data:\n", "-" * 78, "\n", data | |
51 | return data | |
52 | ||
53 | ############################################################################## | |
54 | ||
55 | def main(ctx, action): | |
56 | data = clipboard_data(ctx) | |
57 | ||
58 | numeric_pattern = re.compile('^[+-]?[0-9]+([\. ][0-9][0-9][0-9])*([\.,][0-9]+)?$') | |
59 | date_pattern = re.compile('^([0-9]?[0-9])/([0-9]?[0-9])/([0-9]?[0-9]?[0-9]?[0-9])$') | |
60 | ||
61 | if action == 'cumuls': | |
62 | sheet_name = u'CODA-Cumuls' | |
63 | # on découpe le texte reçu en lignes puis en colonnes | |
64 | # on ne garde ici que les 10 premières colonnes | |
65 | data = [row.split('\t')[:10] for row in data.splitlines()] | |
66 | data[0][6] = '' # suppression de la 2nde colonne 'Engagement' | |
67 | data[0][8] = '' # suppression de la colonne 'Colonne9' | |
68 | ||
b8c5c6f9 | 69 | elif action == 'details' or action == 'details-encore': |
b56e39ea P |
70 | sheet_name = u'CODA-Détails' |
71 | # on découpe le texte reçu en lignes puis en colonnes | |
72 | data = [row.split('\t') for row in data.splitlines()] | |
73 | ||
74 | else: | |
75 | raise RuntimeError, u"Action inconnue (erreur de programmation)." | |
76 | ||
77 | # on efface la dernière ligne pleine de car. nuls, le cas échéant | |
78 | last = data[len(data)-1] | |
79 | if len(last) == 1 and last[0].strip('\x00') == '': | |
80 | del data[len(data)-1] | |
81 | ||
b8c5c6f9 P |
82 | # on détermine le type des données, par colonne : |
83 | # - on vérifie si toutes les valeurs de la colonne sont identiques | |
84 | # - on vérifie si la colonne ne contient que des dates | |
85 | # - on vérifie si la colonne ne contient que des nombres | |
86 | column_type = [str] * len(data[0]) | |
b56e39ea | 87 | for column in range(len(data[0])-1,-1,-1): |
b8c5c6f9 P |
88 | # quelques colonnes ne doivent pas être testées |
89 | if action in ('details','details-encore') and data[0][column] in ('Num doc','PCG'): | |
90 | continue | |
91 | # on prend la première valeur, nettoyée un peu pour l'analyse | |
92 | first = data[1][column].strip().replace(u'\xa0','') | |
93 | flag_date = first and re.match(date_pattern, first) | |
94 | flag_numeric = first and first != '#DEV!' and re.match(numeric_pattern, first) | |
b56e39ea | 95 | diff = False |
b8c5c6f9 P |
96 | # parcours des autres valeurs de la colonne |
97 | for row in range(2,len(data)): | |
98 | value = data[row][column].strip().replace(u'\xa0','') | |
99 | if value: | |
100 | if not diff and value != first: | |
101 | diff = True | |
102 | if flag_date and not re.match(date_pattern, value): | |
103 | flag_date = False | |
104 | if flag_numeric and value != '#DEV!' and not re.match(numeric_pattern, value): | |
105 | flag_numeric = False | |
b56e39ea P |
106 | # s'il y a au moins 2 lignes et que les valeurs dans la colonne |
107 | # sont toutes identiques et qu'il n'y a pas d'intitulé sur la | |
108 | # colonne alors on supprime toute la colonne | |
109 | if len(data) > 2 and not diff and not data[0][column]: | |
110 | for row in range(len(data)): | |
111 | del data[row][column] | |
b8c5c6f9 P |
112 | del column_type[column] |
113 | continue | |
114 | # si la colonne contient des dates, conversion des valeurs comme telles | |
115 | if flag_date: | |
116 | for row in range(1,len(data)): | |
117 | value = data[row][column].strip().replace(u'\xa0','') | |
118 | if value and value != '#DEV!': | |
119 | m = re.match(date_pattern, value) | |
120 | if m: | |
121 | (day, month, year) = [int(x) for x in m.groups()] | |
122 | if year < 50: | |
123 | year += 2000 | |
124 | elif year < 100: | |
125 | year += 1900 | |
126 | value = '%02d/%02d/%04d' % (month, day, year) | |
127 | data[row][column] = value | |
128 | column_type[column] = datetime.date | |
129 | continue | |
130 | # si la colonne est numérique, conversion des valeurs comme telles | |
131 | if flag_numeric: | |
132 | for row in range(1,len(data)): | |
133 | value = data[row][column].strip().replace(u'\xa0','') | |
134 | if value and value != '#DEV!': | |
135 | data[row][column] = float(value.replace('.','').replace(',','.')) | |
136 | column_type[column] = float | |
137 | continue | |
b56e39ea P |
138 | |
139 | #print "Data:\n", "-" * 78, "\n", data | |
140 | ||
141 | # récupération du document en cours, vérification du type Spreadsheet | |
142 | desktop = ctx.ServiceManager.createInstanceWithContext( | |
143 | "com.sun.star.frame.Desktop", ctx) | |
144 | document = desktop.getCurrentComponent() | |
145 | if not document.supportsService("com.sun.star.sheet.SpreadsheetDocument"): | |
146 | raise RuntimeError, u"Ce n'est pas un document de type Spreadsheet (Calc)." | |
147 | ||
148 | # basculement vers la feuille de calcul concernée, création au besoin | |
149 | sheets = document.getSheets() | |
150 | try: | |
151 | sheet = sheets.getByName(sheet_name) | |
152 | new_sheet = False | |
153 | except NoSuchElementException, e: | |
154 | sheets.insertNewByName(sheet_name, 0) | |
155 | sheet = sheets.getByName(sheet_name) | |
156 | new_sheet = True | |
157 | controller = document.getCurrentController() | |
158 | controller.setActiveSheet(sheet) | |
159 | ||
b8c5c6f9 | 160 | # récupération des formats date, monétaire et nombre en français |
a69abe13 P |
161 | # http://api.openoffice.org/docs/common/ref/com/sun/star/util/XNumberFormatTypes.html |
162 | # http://api.openoffice.org/docs/common/ref/com/sun/star/i18n/NumberFormatIndex.html | |
b56e39ea P |
163 | locale_fr_FR = Locale('fr', 'FR', '') |
164 | number_formats = document.getNumberFormats() | |
b56e39ea | 165 | date_format = number_formats.getStandardFormat(DATE, locale_fr_FR) |
b8c5c6f9 P |
166 | currency_format = number_formats.getStandardFormat(CURRENCY, locale_fr_FR) |
167 | number_format = number_formats.getFormatIndex(NUMBER_1000DEC2, locale_fr_FR) | |
168 | #format_string = number_formats.generateFormat( | |
169 | # NUMBER, locale_fr_FR, True, False, 2, 1) | |
170 | #format_key = number_formats.queryKey(format_string, locale_fr_FR, True) | |
171 | #if format_key < 0: | |
172 | # format_key = number_formats.addNew(format_string, locale_fr_FR) | |
173 | #print "NUMBER_1000DEC2=%s key=%s" % (NUMBER_1000DEC2, format_key) | |
174 | #number_format = number_formats.getByKey(format_key) | |
175 | ||
176 | # détermination de la zone de travail | |
177 | if action.endswith('-encore'): | |
178 | # localisation de la la zone déjà utilisée | |
179 | cursor = sheet.createCursor() | |
180 | cursor.gotoEndOfUsedArea(False) | |
181 | cursor.gotoStartOfUsedArea(True) | |
182 | rangeAddress = cursor.getRangeAddress() | |
183 | row = rangeAddress.EndRow | |
184 | # FIXME: on devrait comparer ici rangeAddress.EndColumn avec len(data[0]) | |
185 | else: | |
186 | row = 0 | |
b56e39ea | 187 | |
b8c5c6f9 P |
188 | cell_range = sheet.getCellRangeByPosition( |
189 | 0, row, len(data[0])-1, row + len(data)-1) | |
b56e39ea P |
190 | cursor = sheet.createCursorByRange(cell_range) |
191 | ||
192 | # insertion des données | |
193 | for row in range(1, len(data)): | |
194 | for column in range(len(data[row])): | |
195 | value = data[row][column] | |
196 | cell = cursor.getCellByPosition(column, row) | |
b8c5c6f9 | 197 | if column_type[column] == float and value != '#DEV!': |
b56e39ea | 198 | cell.Value = value |
b56e39ea | 199 | else: |
b56e39ea | 200 | cell.Formula = value |
b8c5c6f9 | 201 | |
b56e39ea | 202 | # insertion des en-têtes, après les données pour un autofit correct |
b8c5c6f9 P |
203 | if new_sheet: |
204 | for column in range(len(data[0])): | |
205 | # écriture de l'en-tête de la colonne | |
206 | cell = cursor.getCellByPosition(column, 0) | |
207 | cell.Formula = data[0][column] | |
208 | # formatage par défaut pour la colonne | |
209 | sheet_column = sheet.Columns.getByIndex(column) | |
210 | if column_type[column] == datetime.date: | |
211 | sheet_column.NumberFormat = date_format | |
212 | elif column_type[column] == float: | |
213 | if action == 'cumuls': | |
214 | sheet_column.NumberFormat = currency_format | |
215 | else: | |
4928a69a | 216 | if data[0][column] in COLONNES_NUMERIQUES: |
b8c5c6f9 P |
217 | sheet_column.NumberFormat = number_format |
218 | else: | |
219 | sheet_column.NumberFormat = currency_format | |
220 | # ajustement automatique, une fois toutes les valeurs écrites | |
221 | sheet_column.OptimalWidth = True | |
222 | # formatage spécifique pour la ligne d'en-têtes | |
223 | headers_row = sheet.Rows.getByIndex(0) | |
224 | headers_row.ParaAdjust = 3 # CENTER | |
b56e39ea P |
225 | |
226 | ############################################################################## | |
227 | ||
228 | # Show a message box with the UNO based toolkit | |
229 | def messageBox(ctx, message): | |
230 | document = XSCRIPTCONTEXT.getDocument() | |
231 | window = document.CurrentController.Frame.ContainerWindow | |
232 | ||
233 | aDescriptor = WindowDescriptor() | |
234 | aDescriptor.Type = MODALTOP | |
235 | aDescriptor.WindowServiceName = "infobox" | |
236 | aDescriptor.ParentIndex = -1 | |
237 | aDescriptor.Parent = window | |
238 | #aDescriptor.Bounds = Rectangle() | |
239 | aDescriptor.WindowAttributes = OK | |
240 | ||
241 | tk = window.getToolkit() | |
242 | msgbox = tk.createWindow(aDescriptor) | |
243 | msgbox.setCaptionText("Collage d'interrogation CODA") | |
244 | msgbox.setMessageText(unicode(message)) | |
245 | ||
246 | return msgbox.execute() | |
247 | ||
248 | def coller_cumuls(event=False): | |
249 | u"""Coller une interrogation des cumuls CODA.""" | |
250 | ctx = uno.getComponentContext() | |
251 | try: | |
252 | main(ctx, 'cumuls') | |
253 | except Exception, e: | |
254 | messageBox(ctx, e) | |
255 | return None | |
256 | ||
257 | def coller_details(event=False): | |
258 | u"""Coller une interrogation des détails CODA.""" | |
259 | ctx = uno.getComponentContext() | |
260 | try: | |
261 | main(ctx, 'details') | |
262 | except Exception, e: | |
263 | messageBox(ctx, e) | |
264 | return None | |
265 | ||
b8c5c6f9 P |
266 | def coller_details_encore(event=False): |
267 | u"""Coller une interrogation des détails CODA supplémentaire.""" | |
268 | ctx = uno.getComponentContext() | |
269 | try: | |
270 | main(ctx, 'details-encore') | |
271 | except Exception, e: | |
272 | messageBox(ctx, e) | |
273 | return None | |
274 | ||
275 | g_exportedScripts = (coller_cumuls, coller_details, coller_details_encore) | |
b56e39ea P |
276 | |
277 | ############################################################################## | |
278 | ||
279 | from com.sun.star.task import XJobExecutor | |
280 | ||
281 | class CollerCumulsJob(unohelper.Base, XJobExecutor): | |
282 | def __init__(self, context): | |
283 | self._context = context | |
284 | ||
285 | def trigger(self, args): | |
286 | try: | |
287 | main(self._context, 'cumuls') | |
288 | except Exception, e: | |
289 | messageBox(self._context, e) | |
290 | ||
291 | class CollerDetailsJob(unohelper.Base, XJobExecutor): | |
292 | def __init__(self, context): | |
293 | self._context = context | |
294 | ||
295 | def trigger(self, args): | |
296 | try: | |
297 | main(self._context, 'details') | |
298 | except Exception, e: | |
299 | messageBox(self._context, e) | |
300 | ||
b8c5c6f9 P |
301 | class CollerDetailsEncoreJob(unohelper.Base, XJobExecutor): |
302 | def __init__(self, context): | |
303 | self._context = context | |
304 | ||
305 | def trigger(self, args): | |
306 | try: | |
307 | main(self._context, 'details-encore') | |
308 | except Exception, e: | |
309 | messageBox(self._context, e) | |
310 | ||
b56e39ea P |
311 | g_ImplementationHelper = unohelper.ImplementationHelper() |
312 | g_ImplementationHelper.addImplementation( \ | |
313 | CollerCumulsJob, "org.auf.openoffice.CODA.CollerCumuls", \ | |
314 | ("com.sun.star.task.Job",),) | |
315 | g_ImplementationHelper.addImplementation( \ | |
316 | CollerDetailsJob, "org.auf.openoffice.CODA.CollerDetails", \ | |
317 | ("com.sun.star.task.Job",),) | |
b8c5c6f9 P |
318 | g_ImplementationHelper.addImplementation( \ |
319 | CollerDetailsEncoreJob, "org.auf.openoffice.CODA.CollerDetailsEncore", \ | |
320 | ("com.sun.star.task.Job",),) |