From ef94f641b5053f33e6d3537215812d1fc671882d Mon Sep 17 00:00:00 2001 From: Vincent Texier <vit@free.fr> Date: Wed, 2 Apr 2025 17:04:57 +0200 Subject: [PATCH] [feat] add Export as OFX in transfer_history_menu --- tikka/adapters/network/indexer/transfers.py | 146 +++++++++--------- tikka/domains/application.py | 3 +- tikka/domains/indexers.py | 22 ++- tikka/domains/nodes.py | 22 ++- tikka/domains/transfers.py | 133 ++++++++++++++++ .../adapters/network/indexer/transfers.py | 10 +- tikka/slots/pyqt/entities/constants.py | 3 + .../resources/gui/windows/transfers_export.ui | 55 +++++++ .../pyqt/widgets/transfer_history_menu.py | 15 ++ tikka/slots/pyqt/windows/transfers_export.py | 145 +++++++++++++++++ 10 files changed, 447 insertions(+), 107 deletions(-) create mode 100644 tikka/slots/pyqt/resources/gui/windows/transfers_export.ui create mode 100644 tikka/slots/pyqt/windows/transfers_export.py diff --git a/tikka/adapters/network/indexer/transfers.py b/tikka/adapters/network/indexer/transfers.py index c205fdcd..dea28ec5 100644 --- a/tikka/adapters/network/indexer/transfers.py +++ b/tikka/adapters/network/indexer/transfers.py @@ -34,84 +34,82 @@ class IndexerTransfers(IndexerTransfersInterface): def list( self, - address, + address: str, limit: int, + offset: int = 0, sort_column: str = IndexerTransfersInterface.COLUMN_TIMESTAMP, sort_order: str = IndexerTransfersInterface.SORT_ORDER_DESCENDING, - since: Optional[datetime] = None, + from_datetime: Optional[datetime] = None, + to_datetime: Optional[datetime] = None, ) -> List[Transfer]: __doc__ = ( # pylint: disable=redefined-builtin, unused-variable IndexerTransfersInterface.list.__doc__ ) + if ( not self.indexer.connection.is_connected() or self.indexer.connection.client is None ): raise IndexerTransfersException(NetworkConnectionError()) - since_str = "" - if since is not None: - since_str = ( - """_and: { - timestamp: {_gte: \"""" - + since.isoformat() - + """\"} - }""" + # Construire la condition de filtrage pour la date + date_filter = [] + if from_datetime: + date_filter.append( + f'{{ timestamp: {{ _gte: "{from_datetime.isoformat()}" }} }}' + ) + if to_datetime: + date_filter.append( + f'{{ timestamp: {{ _lte: "{to_datetime.isoformat()}" }} }}' ) + date_filter_str = ( + "_and: [" + ", ".join(date_filter) + "]" if date_filter else "" + ) + + # Construire la requête GraphQL query = gql( - """ - query { + f""" + query {{ transfer( - limit: """ - + str(limit) - + """ - orderBy: { - """ - + sort_column - + """: """ - + sort_order - + """ - } - where: { + limit: {limit} + offset: {offset} + orderBy: {{ + {sort_column}: {sort_order} + }} + where: {{ _or: [ - {fromId: {_eq: \"""" - + address - + """\"}} - {toId: {_eq: \"""" - + address - + """\"}} + {{ fromId: {{ _eq: \"{address}\" }} }} + {{ toId: {{ _eq: \"{address}\" }} }} ] - """ - + since_str - + """ - } - ) { + {date_filter_str} + }} + ) {{ id fromId - from { - identity { + from {{ + identity {{ index name - } - } + }} + }} toId - to { - identity { + to {{ + identity {{ index name - } - } + }} + }} amount timestamp - comment { + comment {{ type remark remarkBytes - } - } - } - """ + }} + }} + }} + """ ) try: @@ -119,43 +117,38 @@ class IndexerTransfers(IndexerTransfersInterface): except Exception as exception: raise NetworkIndexerException(exception) - # {'transfer': [ - # {'id': '0000056159-1d1ed-000003', - # 'fromId': '5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz', - # 'from': {'identity': {'index': 344, 'name': 'xxxx'}}, - # 'toId': '5CQ8T4qpbYJq7uVsxGPQ5q2df7x3Wa4aRY6HUWMBYjfLZhnn', - # 'to': {'identity': {'index': 61, 'name': 'yyyy'}}, - # 'amount': 123, - # 'timestamp': '2024-02-08T14:59:00.001+00:00', - # 'comment': {'remark': 'test 2', 'type': 'ASCII'}, - # ]} - transfers = [] - for transfer in result["transfer"]: - if transfer["from"]["identity"] is not None: - issuer_identity_index = transfer["from"]["identity"]["index"] - issuer_identity_name = transfer["from"]["identity"]["name"] - else: - issuer_identity_index = None - issuer_identity_name = None - if transfer["to"]["identity"] is not None: - receiver_identity_index = transfer["to"]["identity"]["index"] - receiver_identity_name = transfer["to"]["identity"]["name"] - else: - receiver_identity_index = None - receiver_identity_name = None - - if transfer["comment"] is not None: + issuer_identity_index = ( + transfer["from"]["identity"]["index"] + if transfer["from"]["identity"] + else None + ) + issuer_identity_name = ( + transfer["from"]["identity"]["name"] + if transfer["from"]["identity"] + else None + ) + receiver_identity_index = ( + transfer["to"]["identity"]["index"] + if transfer["to"]["identity"] + else None + ) + receiver_identity_name = ( + transfer["to"]["identity"]["name"] + if transfer["to"]["identity"] + else None + ) + + comment = None + comment_type = None + if transfer["comment"]: comment = ( transfer["comment"]["remarkBytes"] if transfer["comment"]["type"] == "RAW" else transfer["comment"]["remark"] ) comment_type = transfer["comment"]["type"] - else: - comment = None - comment_type = None transfers.append( Transfer( @@ -172,6 +165,7 @@ class IndexerTransfers(IndexerTransfersInterface): comment_type=comment_type, ) ) + return transfers def count(self, address) -> int: diff --git a/tikka/domains/application.py b/tikka/domains/application.py index daea2853..20752bee 100644 --- a/tikka/domains/application.py +++ b/tikka/domains/application.py @@ -116,6 +116,7 @@ class Application: self.transfers = Transfers( self.wallets, self.repository.transfers, + self.currencies, self.network.node.transfers, self.network.indexer.transfers, self.event_dispatcher, @@ -139,7 +140,6 @@ class Application: self.nodes = Nodes( self.repository.nodes, self.preferences, - self.connections, self.network.node, self.config, self.currencies, @@ -148,7 +148,6 @@ class Application: self.indexers = Indexers( self.repository.indexers, self.preferences, - self.connections, self.network.indexer, self.config, self.currencies, diff --git a/tikka/domains/indexers.py b/tikka/domains/indexers.py index a5db9e6b..2af4aa8d 100644 --- a/tikka/domains/indexers.py +++ b/tikka/domains/indexers.py @@ -21,7 +21,6 @@ from urllib import request from tikka.adapters.network.indexer.indexer import NetworkIndexer from tikka.domains.config import Config -from tikka.domains.connections import Connections from tikka.domains.currencies import Currencies from tikka.domains.entities.constants import ( INDEXERS_CURRENT_ENTRY_POINT_URL_PREFERENCES_KEY, @@ -46,8 +45,7 @@ class Indexers: self, repository: IndexersRepositoryInterface, preferences: Preferences, - connections: Connections, - network: NetworkIndexerInterface, + network_indexer: NetworkIndexerInterface, config: Config, currencies: Currencies, event_dispatcher: EventDispatcher, @@ -57,16 +55,14 @@ class Indexers: :param repository: EntryPointsRepositoryInterface instance :param preferences: Preferences domain instance - :param connections: Connections instance - :param network: Network adapter instance for handling indexers + :param network_indexer: Network adapter instance for handling indexers :param config: Config instance :param currencies: Currencies instance :param event_dispatcher: EventDispatcher instance """ self.repository = repository self.preferences = preferences - self.connections = connections - self.network = network + self.network_indexer = network_indexer self.config = config self.currencies = currencies self.event_dispatcher = event_dispatcher @@ -79,7 +75,7 @@ class Indexers: # events self.event_dispatcher.add_event_listener( ConnectionsEvent.EVENT_TYPE_INDEXER_CONNECTED, - self._on_connections_connected, + self._on_indexer_connected_event, ) def init_repository(self): @@ -135,7 +131,7 @@ class Indexers: :return: """ current_indexer = self.repository.get(self.get_current_url()) - network_indexer = self.network.get() + network_indexer = self.network_indexer.get() if network_indexer is None: return None @@ -180,7 +176,7 @@ class Indexers: # never choose localhost randomly... if "localhost" not in url: self.set_current_url(self.list()[index].url) - if self.connections.indexer.is_connected(): + if self.network_indexer.connection.is_connected(): break def add(self, indexer: Indexer) -> None: @@ -291,12 +287,12 @@ class Indexers: self._current_url, ) # switch current connection - self.connections.indexer.disconnect() + self.network_indexer.connection.disconnect() indexer = self.get(self._current_url) if indexer is not None: - self.connections.indexer.connect(indexer) + self.network_indexer.connection.connect(indexer) - def _on_connections_connected(self, _: ConnectionsEvent): + def _on_indexer_connected_event(self, _: ConnectionsEvent): """ Triggered when the connection is established diff --git a/tikka/domains/nodes.py b/tikka/domains/nodes.py index 2f4b9dd4..d1954d44 100644 --- a/tikka/domains/nodes.py +++ b/tikka/domains/nodes.py @@ -21,7 +21,6 @@ from urllib import request from tikka.adapters.network.node.node import NetworkNode from tikka.domains.config import Config -from tikka.domains.connections import Connections from tikka.domains.currencies import Currencies from tikka.domains.entities.constants import ( NODES_CURRENT_ENTRY_POINT_URL_PREFERENCES_KEY, @@ -46,8 +45,7 @@ class Nodes: self, repository: NodesRepositoryInterface, preferences: Preferences, - connections: Connections, - network: NetworkNodeInterface, + network_node: NetworkNodeInterface, config: Config, currencies: Currencies, event_dispatcher: EventDispatcher, @@ -57,16 +55,14 @@ class Nodes: :param repository: EntryPointsRepositoryInterface instance :param preferences: Preferences domain instance - :param connections: Connections instance - :param network: NetworkNodeInterface adapter instance for handling nodes + :param network_node: NetworkNodeInterface adapter instance for handling nodes :param config: Config instance :param currencies: Currencies instance :param event_dispatcher: EventDispatcher instance """ self.repository = repository self.preferences = preferences - self.connections = connections - self.network = network + self.network_node = network_node self.config = config self.currencies = currencies self.event_dispatcher = event_dispatcher @@ -78,7 +74,7 @@ class Nodes: # events self.event_dispatcher.add_event_listener( - ConnectionsEvent.EVENT_TYPE_NODE_CONNECTED, self._on_connections_connected + ConnectionsEvent.EVENT_TYPE_NODE_CONNECTED, self._on_node_connected_event ) def init_repository(self): @@ -138,7 +134,7 @@ class Nodes: :return: """ current_node = self.repository.get(self.get_current_url()) - network_node = self.network.get() + network_node = self.network_node.get() if network_node is None: return None @@ -191,7 +187,7 @@ class Nodes: # never choose localhost randomly... if "localhost" not in url: self.set_current_url(self.list()[index].url) - if self.connections.node.is_connected(): + if self.network_node.connection.is_connected(): break def add(self, node: Node) -> None: @@ -297,12 +293,12 @@ class Nodes: self._current_url, ) # switch current connection - self.connections.node.disconnect() + self.network_node.connection.disconnect() node = self.get(self._current_url) if node is not None: - self.connections.node.connect(node) + self.network_node.connection.connect(node) - def _on_connections_connected(self, _: ConnectionsEvent): + def _on_node_connected_event(self, _: ConnectionsEvent): """ Triggered when the connection is established diff --git a/tikka/domains/transfers.py b/tikka/domains/transfers.py index 88dac0a7..eeb7a7e3 100644 --- a/tikka/domains/transfers.py +++ b/tikka/domains/transfers.py @@ -12,9 +12,12 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import logging +from datetime import datetime from typing import Any, Dict, List, Optional from tikka.adapters.network.node.substrate_client import ExtrinsicReceipt +from tikka.domains.currencies import Currencies from tikka.domains.entities.account import Account from tikka.domains.entities.events import TransferEvent from tikka.domains.entities.transfer import Transfer @@ -28,6 +31,16 @@ from tikka.interfaces.adapters.repository.transfers import TransfersRepositoryIn from tikka.libs.keypair import Keypair +def format_datetime(dt: datetime) -> str: + """ + Formate une date en OFX (YYYYMMDDHHMMSS). + + :param dt: Datetime data + :return: + """ + return dt.strftime("%Y%m%d%H%M%S") + + class Transfers: """ Transfers domain class @@ -37,6 +50,7 @@ class Transfers: self, wallets: Wallets, repository: TransfersRepositoryInterface, + currencies: Currencies, node_transfers: NodeTransfersInterface, indexer_transfers: IndexerTransfersInterface, event_dispatcher: EventDispatcher, @@ -46,11 +60,13 @@ class Transfers: :param wallets: Wallets domain :param repository: TransfersRepositoryInterface adapter instance + :param currencies: Currencies domain instance :param node_transfers: NodeTransfersInterface adapter instance :param event_dispatcher: EventDispatcher instance """ self.wallets = wallets self.repository = repository + self.currencies = currencies self.node_transfers = node_transfers self.indexer_transfers = indexer_transfers self.event_dispatcher = event_dispatcher @@ -225,3 +241,120 @@ class Transfers: self.event_dispatcher.dispatch_event(event) return receipt + + def export_as_ofx( + self, + filepath: str, + address: str, + from_datetime: Optional[datetime] = None, + to_datetime: Optional[datetime] = None, + ): + """ + Export transfers for address in an OFX format file + + :param filepath: Path of the file + :param address: Account address + :param from_datetime: Optional period start date + :param to_datetime: Optional period end date + :return: + """ + batch_size = 100 + offset = 0 + + with open(filepath, "w", encoding="utf-8") as f: + # Écriture de l'en-tête OFX + f.write( + f"""OFXHEADER:100 + DATA:OFXSGML + VERSION:102 + SECURITY:NONE + ENCODING:USASCII + CHARSET:1252 + COMPRESSION:NONE + OLDFILEUID:NONE + NEWFILEUID:NONE + + <OFX> + <BANKMSGSRSV1> + <STMTTRNRS> + <STMTRS> + <CURDEF>EUR</CURDEF> + <BANKACCTFROM> + <BANKID>{self.currencies.get_current().name}</BANKID> + <ACCTID>{address}</ACCTID> + <ACCTTYPE>CHECKING</ACCTTYPE> + </BANKACCTFROM> + <BANKTRANLIST> + """ + ) + + while True: + # Récupérer un lot de 100 transferts depuis GraphQL + transfers = self.indexer_transfers.list( + address=address, + limit=batch_size, + offset=offset, + from_datetime=from_datetime, + to_datetime=to_datetime, + ) + + if not transfers: + break # Arrêter si plus de transferts à récupérer + + total_amount: float = 0.0 + for transfer in transfers: + if transfer.issuer_address != address: + amount_sign = 1 + name = ( + f"{transfer.issuer_address} - {transfer.issuer_identity_name}#{transfer.issuer_identity_index}" + if transfer.issuer_identity_name + else transfer.issuer_address + ) + else: + amount_sign = -1 + name = ( + f"{transfer.receiver_address} - {transfer.receiver_identity_name}#{transfer.receiver_identity_index}" + if transfer.receiver_identity_name + else transfer.receiver_address + ) + total_amount += ( + transfer.amount / 100 + ) * amount_sign # Convert cents to dollars + + f.write( + f""" + <STMTTRN> + <TRNTYPE>XFER</TRNTYPE> + <DTPOSTED>{format_datetime(transfer.timestamp)}</DTPOSTED> + <TRNAMT>{(transfer.amount / 100)*amount_sign:.2f}</TRNAMT> + <FITID>{transfer.id}</FITID> + <NAME>{name}</NAME> + <MEMO>{transfer.comment or ''}</MEMO> + </STMTTRN> + """ + ) + + offset += batch_size # Passer au lot suivant + + current_datetime = format_datetime(datetime.utcnow()) + + # Écriture du pied de fichier OFX + f.write( + f""" + </BANKTRANLIST> + <LEDGERBAL> + <BALAMT>{total_amount:.2f}</BALAMT> + <DTASOF>{current_datetime}</DTASOF> + </LEDGERBAL> + <AVAILBAL> + <BALAMT>{total_amount:.2f}</BALAMT> + <DTASOF>{current_datetime}</DTASOF> + </AVAILBAL> + </STMTRS> + </STMTTRNRS> + </BANKMSGSRSV1> + </OFX> + """ + ) + + logging.debug(f"Export OFX terminé : {filepath}") diff --git a/tikka/interfaces/adapters/network/indexer/transfers.py b/tikka/interfaces/adapters/network/indexer/transfers.py index 4e01bda9..5ba17b6d 100644 --- a/tikka/interfaces/adapters/network/indexer/transfers.py +++ b/tikka/interfaces/adapters/network/indexer/transfers.py @@ -43,20 +43,24 @@ class IndexerTransfersInterface(abc.ABC): @abc.abstractmethod def list( self, - address, + address: str, limit: int, + offset: int = 0, sort_column: str = COLUMN_TIMESTAMP, sort_order: str = SORT_ORDER_DESCENDING, - since: Optional[datetime] = None, + from_datetime: Optional[datetime] = None, + to_datetime: Optional[datetime] = None, ) -> List[Transfer]: """ Return list of transfers from and to address :param address: Account address :param limit: Max number of transfers to return + :param offset: Offset to paginate results :param sort_column: Sort column, default to IndexerTransfersInterface.COLUMN_TIMESTAMP :param sort_order: Sort order, default to IndexerTransfersInterface.SORT_ORDER_DESCENDING - :param since: Only transfers since datetime, default to None + :param from_datetime: Only transfers from datetime, default to None + :param to_datetime: Only transfers until datetime, default to None :return: """ raise NotImplementedError diff --git a/tikka/slots/pyqt/entities/constants.py b/tikka/slots/pyqt/entities/constants.py index fc15cd98..68991c78 100644 --- a/tikka/slots/pyqt/entities/constants.py +++ b/tikka/slots/pyqt/entities/constants.py @@ -60,3 +60,6 @@ INDEXERS_TABLE_SORT_COLUMN_PREFERENCES_KEY = "indexers_table_sort_column" INDEXERS_TABLE_SORT_ORDER_PREFERENCES_KEY = "indexers_table_sort_order" TABS_PREFERENCES_KEY = "tabs_opened" SAVE_DATA_DEFAULT_DIR_PREFERENCES_KEY = "save_data_default_dir" +TRANSFERS_EXPORT_DEFAULT_DIRECTORY_PREFERENCES_KEY = ( + "transfers_export_default_directory" +) diff --git a/tikka/slots/pyqt/resources/gui/windows/transfers_export.ui b/tikka/slots/pyqt/resources/gui/windows/transfers_export.ui new file mode 100644 index 00000000..b77e8ad5 --- /dev/null +++ b/tikka/slots/pyqt/resources/gui/windows/transfers_export.ui @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>transfersExportDialog</class> + <widget class="QDialog" name="transfersExportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>801</width> + <height>457</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export as</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="fromLabel"> + <property name="text"> + <string>From</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCalendarWidget" name="fromCalendarWidget"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="toLabel"> + <property name="text"> + <string>To</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCalendarWidget" name="toCalendarWidget"/> + </item> + <item row="2" column="1"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="todayButton"> + <property name="text"> + <string>Today</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/tikka/slots/pyqt/widgets/transfer_history_menu.py b/tikka/slots/pyqt/widgets/transfer_history_menu.py index c2b068cf..961bf48e 100644 --- a/tikka/slots/pyqt/widgets/transfer_history_menu.py +++ b/tikka/slots/pyqt/widgets/transfer_history_menu.py @@ -24,6 +24,7 @@ from tikka.domains.entities.account import Account from tikka.domains.entities.constants import DATA_PATH from tikka.domains.entities.transfer import Transfer from tikka.slots.pyqt.windows.transfer import TransferWindow +from tikka.slots.pyqt.windows.transfers_export import TransfersExportWindow class TransferHistoryPopupMenu(QMenu): @@ -96,6 +97,10 @@ class TransferHistoryPopupMenu(QMenu): ) add_account_address_action.triggered.connect(self.add_address_to_accounts) + if self.application.connections.indexer.is_connected(): + export_ofx_action = self.addAction(self._("Export in OFX file")) + export_ofx_action.triggered.connect(self.export_ofx) + def copy_address_to_clipboard(self): """ Copy address of selected row to clipboard @@ -149,6 +154,16 @@ class TransferHistoryPopupMenu(QMenu): self.application.accounts.add(self.contact_account) + def export_ofx(self): + """ + Export transfers in an OFX file + + :return: + """ + TransfersExportWindow( + self.application, self.account.address, self.parentWidget() + ).exec_() + if __name__ == "__main__": qapp = QApplication(sys.argv) diff --git a/tikka/slots/pyqt/windows/transfers_export.py b/tikka/slots/pyqt/windows/transfers_export.py new file mode 100644 index 00000000..6ed7422b --- /dev/null +++ b/tikka/slots/pyqt/windows/transfers_export.py @@ -0,0 +1,145 @@ +# Copyright 2021 Vincent Texier <vit@free.fr> +# +# This software is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from pathlib import Path +from typing import Optional + +from PyQt5.QtCore import QDate +from PyQt5.QtWidgets import QDialog, QFileDialog, QWidget + +from tikka.domains.application import Application +from tikka.slots.pyqt.entities.constants import ( + TRANSFERS_EXPORT_DEFAULT_DIRECTORY_PREFERENCES_KEY, +) +from tikka.slots.pyqt.resources.gui.windows.transfers_export_rc import ( + Ui_transfersExportDialog, +) + + +class TransfersExportWindow(QDialog, Ui_transfersExportDialog): + """ + TransfersExportWindow class + """ + + MIN_PERIOD_IN_WEEKS = 4 + MAX_PERIOD_IN_WEEKS = 104 + + def __init__( + self, application: Application, address: str, parent: Optional[QWidget] = None + ): + """ + Init transfers export window instance + + :param application: Application instance + :param address: Account address + :param parent: QWidget instance + """ + super().__init__(parent=parent) + self.setupUi(self) + + self.application = application + self._ = self.application.translator.gettext + self.address = address + + # buttons + self.buttonBox.accepted.connect(self.on_accepted_button) + self.buttonBox.rejected.connect(self.close) + self.fromCalendarWidget.selectionChanged.connect(self.update_date_constraints) + self.toCalendarWidget.selectionChanged.connect(self.update_date_constraints) + self.todayButton.clicked.connect(self.init_calendar_limits) + self.init_calendar_limits() + + def init_calendar_limits(self): + """ + Définit les dates minimales et maximales autorisées pour les calendriers, + en se basant sur la période maximale définie. + """ + today = QDate.currentDate() + default_min_date = today.addDays(-self.MIN_PERIOD_IN_WEEKS * 7) + + # Définir la plage de dates pour le calendrier "from" + # self.fromCalendarWidget.setDateRange(max_min_date, today) + self.fromCalendarWidget.setSelectedDate(default_min_date) + + # Définir la plage de dates pour le calendrier "to" + # self.toCalendarWidget.setDateRange(min_date, max_date) + self.toCalendarWidget.setSelectedDate(today) + + def update_date_constraints(self): + """ + Update calendars with date constraints + """ + from_date = self.fromCalendarWidget.selectedDate() + to_date = self.toCalendarWidget.selectedDate() + min_to_date = from_date.addDays(self.MIN_PERIOD_IN_WEEKS * 7) + max_from_date = to_date.addDays(-self.MAX_PERIOD_IN_WEEKS * 7) + + if from_date > to_date: + self.fromCalendarWidget.setSelectedDate(to_date) + elif to_date < min_to_date: + self.toCalendarWidget.setSelectedDate(min_to_date) + + max_to_date = from_date.addDays(self.MAX_PERIOD_IN_WEEKS * 7) + + if to_date > max_to_date: + self.toCalendarWidget.setSelectedDate(max_to_date) + if from_date < max_from_date: + self.fromCalendarWidget.setSelectedDate(max_from_date) + # self.toCalendarWidget.setMinimumDate(from_date) + # self.toCalendarWidget.setMaximumDate(max_to_date) + # self.fromCalendarWidget.setMaximumDate(to_date) + + def open_file_dialog(self) -> Optional[str]: + """ + Open file dialog and return the selected filepath or None + + :return: + """ + default_dir = self.application.repository.preferences.get( + TRANSFERS_EXPORT_DEFAULT_DIRECTORY_PREFERENCES_KEY + ) + if default_dir is not None: + default_dir = str(Path(default_dir).expanduser().absolute()) + else: + default_dir = "" + + result = QFileDialog.getSaveFileName( + self, self._("Export file"), default_dir, "OFX Files (*.ofx)" + ) + if result[0] == "": + return None + + self.application.repository.preferences.set( + TRANSFERS_EXPORT_DEFAULT_DIRECTORY_PREFERENCES_KEY, + str(Path(result[0]).parent), + ) + + return result[0] + + def on_accepted_button(self) -> None: + """ + Triggered when user click on ok button + + :return: + """ + # open file dialog + filepath = self.open_file_dialog() + if filepath is not None: + self.application.transfers.export_as_ofx( + filepath, + self.address, + self.fromCalendarWidget.selectedDate().toPyDate(), + self.toCalendarWidget.selectedDate().toPyDate(), + ) + self.close() -- GitLab