From 1f255fbb9f9a7f84bffee3236fd5c0090d5aab79 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 23 Jun 2022 12:56:50 +0300 Subject: [PATCH] Purchase orders --- lib/data/models/models.dart | 1 + lib/data/models/models.g.dart | 4 + .../purchase_order_repository.dart | 84 ++++- .../purchase_order_actions.dart | 29 +- .../purchase_order_middleware.dart | 300 +++++++++++++++--- 5 files changed, 372 insertions(+), 46 deletions(-) diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 66327f86d..2bc5b9501 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -116,6 +116,7 @@ class EntityAction extends EnumClass { static const EntityAction addToInvoice = _$addToInvoice; static const EntityAction cancel = _$cancel; static const EntityAction save = _$save; + static const EntityAction accept = _$accept; @override String toString() { diff --git a/lib/data/models/models.g.dart b/lib/data/models/models.g.dart index 73020ec53..43753160f 100644 --- a/lib/data/models/models.g.dart +++ b/lib/data/models/models.g.dart @@ -86,6 +86,7 @@ const EntityAction _$changeStatus = const EntityAction._('changeStatus'); const EntityAction _$addToInvoice = const EntityAction._('addToInvoice'); const EntityAction _$cancel = const EntityAction._('cancel'); const EntityAction _$save = const EntityAction._('save'); +const EntityAction _$accept = const EntityAction._('accept'); EntityAction _$valueOf(String name) { switch (name) { @@ -227,6 +228,8 @@ EntityAction _$valueOf(String name) { return _$cancel; case 'save': return _$save; + case 'accept': + return _$accept; default: throw new ArgumentError(name); } @@ -303,6 +306,7 @@ final BuiltSet _$values = _$addToInvoice, _$cancel, _$save, + _$accept, ]); Serializer _$entityActionSerializer = diff --git a/lib/data/repositories/purchase_order_repository.dart b/lib/data/repositories/purchase_order_repository.dart index e989d67df..3b4518841 100644 --- a/lib/data/repositories/purchase_order_repository.dart +++ b/lib/data/repositories/purchase_order_repository.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -25,8 +26,20 @@ class PurchaseOrderRepository { return purchaseOrderResponse.data; } - Future> loadList(Credentials credentials) async { - final String url = credentials.url + '/purchase_orders?'; + Future> loadList( + Credentials credentials, + int page, + int createdAt, + bool filterDeleted, + int recordsPerPage, + ) async { + String url = credentials.url + + '/purchase_orders?per_page=$recordsPerPage&page=$page&created_at=$createdAt'; + + if (filterDeleted) { + url += '&filter_deleted_clients=true'; + } + final dynamic response = await webClient.get(url, credentials.token); final InvoiceListResponse purchaseOrderResponse = @@ -52,17 +65,32 @@ class PurchaseOrderRepository { } Future saveData( - Credentials credentials, InvoiceEntity purchaseOrder) async { + Credentials credentials, + InvoiceEntity purchaseOrder, + EntityAction action, + ) async { + purchaseOrder = purchaseOrder.rebuild((b) => b..documents.clear()); final data = serializers.serializeWith(InvoiceEntity.serializer, purchaseOrder); + String url; dynamic response; if (purchaseOrder.isNew) { - response = await webClient.post( - credentials.url + '/purchase_orders', credentials.token, - data: json.encode(data)); + url = credentials.url + '/purchase_orders'; + } else { + url = '${credentials.url}/purchase_orders/${purchaseOrder.id}'; + } + + if (action == EntityAction.markSent) { + url += '&mark_sent=true'; + } else if (action == EntityAction.accept) { + url += '&accept=true'; + } + + if (purchaseOrder.isNew) { + response = + await webClient.post(url, credentials.token, data: json.encode(data)); } else { - final url = '${credentials.url}/purchase_orders/${purchaseOrder.id}'; response = await webClient.put(url, credentials.token, data: json.encode(data)); } @@ -72,4 +100,46 @@ class PurchaseOrderRepository { return purchaseOrderResponse.data; } + + Future emailPurchaseOrder( + Credentials credentials, + InvoiceEntity purchaseOrder, + EmailTemplate template, + String subject, + String body) async { + final data = { + 'entity': '${purchaseOrder.entityType}', + 'entity_id': purchaseOrder.id, + 'template': 'email_template_$template', + 'body': body, + 'subject': subject, + }; + + final dynamic response = await webClient.post( + credentials.url + '/emails', credentials.token, + data: json.encode(data)); + + final InvoiceItemResponse invoiceResponse = + serializers.deserializeWith(InvoiceItemResponse.serializer, response); + + return invoiceResponse.data; + } + + Future uploadDocument(Credentials credentials, + BaseEntity entity, MultipartFile multipartFile) async { + final fields = { + '_method': 'put', + }; + + final dynamic response = await webClient.post( + '${credentials.url}/purchase_orders/${entity.id}/upload', + credentials.token, + data: fields, + multipartFiles: [multipartFile]); + + final InvoiceItemResponse invoiceResponse = + serializers.deserializeWith(InvoiceItemResponse.serializer, response); + + return invoiceResponse.data; + } } diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index 5c421b4a9..fcc8075a8 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -92,9 +93,10 @@ class LoadPurchaseOrderActivity { } class LoadPurchaseOrders { - LoadPurchaseOrders({this.completer}); + LoadPurchaseOrders({this.completer, this.page = 1}); final Completer completer; + final int page; } class LoadPurchaseOrderRequest implements StartLoading {} @@ -145,6 +147,31 @@ class LoadPurchaseOrdersSuccess implements StopLoading { } } +class SavePurchaseOrderDocumentRequest implements StartSaving { + SavePurchaseOrderDocumentRequest({ + @required this.completer, + @required this.multipartFile, + @required this.purchaseOrder, + }); + + final Completer completer; + final MultipartFile multipartFile; + final InvoiceEntity purchaseOrder; +} + +class SavePurchaseOrderDocumentSuccess + implements StopSaving, PersistData, PersistUI { + SavePurchaseOrderDocumentSuccess(this.document); + + final DocumentEntity document; +} + +class SavePurchaseOrderDocumentFailure implements StopSaving { + SavePurchaseOrderDocumentFailure(this.error); + + final Object error; +} + class SavePurchaseOrderRequest implements StartSaving { SavePurchaseOrderRequest({ this.completer, diff --git a/lib/redux/purchase_order/purchase_order_middleware.dart b/lib/redux/purchase_order/purchase_order_middleware.dart index 2dd82cb5a..3fd61a1b6 100644 --- a/lib/redux/purchase_order/purchase_order_middleware.dart +++ b/lib/redux/purchase_order/purchase_order_middleware.dart @@ -1,16 +1,23 @@ +// Flutter imports: import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:invoiceninja_flutter/data/repositories/purchase_order_repository.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_email_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_pdf_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_screen.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/view/purchase_order_view_vm.dart'; + +// Package imports: import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; -import 'package:invoiceninja_flutter/data/models/models.dart'; -import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_screen.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/view/purchase_order_view_vm.dart'; -import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; -import 'package:invoiceninja_flutter/data/repositories/purchase_order_repository.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; List> createStorePurchaseOrdersMiddleware([ PurchaseOrderRepository repository = const PurchaseOrderRepository(), @@ -18,17 +25,28 @@ List> createStorePurchaseOrdersMiddleware([ final viewPurchaseOrderList = _viewPurchaseOrderList(); final viewPurchaseOrder = _viewPurchaseOrder(); final editPurchaseOrder = _editPurchaseOrder(); + final showEmailPurchaseOrder = _showEmailPurchaseOrder(); + final showPdfPurchaseOrder = _showPdfPurchaseOrder(); + final approvePurchaseOrder = _approvePurchaseOrder(repository); final loadPurchaseOrders = _loadPurchaseOrders(repository); final loadPurchaseOrder = _loadPurchaseOrder(repository); final savePurchaseOrder = _savePurchaseOrder(repository); final archivePurchaseOrder = _archivePurchaseOrder(repository); final deletePurchaseOrder = _deletePurchaseOrder(repository); final restorePurchaseOrder = _restorePurchaseOrder(repository); + final emailPurchaseOrder = _emailPurchaseOrder(repository); + final bulkEmailPurchaseOrders = _bulkEmailPurchaseOrders(repository); + final markSentPurchaseOrder = _markSentPurchaseOrder(repository); + final downloadPurchaseOrders = _downloadPurchaseOrders(repository); + final saveDocument = _saveDocument(repository); return [ TypedMiddleware(viewPurchaseOrderList), TypedMiddleware(viewPurchaseOrder), TypedMiddleware(editPurchaseOrder), + TypedMiddleware(approvePurchaseOrder), + TypedMiddleware(showEmailPurchaseOrder), + TypedMiddleware(showPdfPurchaseOrder), TypedMiddleware(loadPurchaseOrders), TypedMiddleware(loadPurchaseOrder), TypedMiddleware(savePurchaseOrder), @@ -37,9 +55,51 @@ List> createStorePurchaseOrdersMiddleware([ TypedMiddleware(deletePurchaseOrder), TypedMiddleware( restorePurchaseOrder), + TypedMiddleware(emailPurchaseOrder), + TypedMiddleware( + bulkEmailPurchaseOrders), + TypedMiddleware( + markSentPurchaseOrder), + TypedMiddleware( + downloadPurchaseOrders), + TypedMiddleware(saveDocument), ]; } +Middleware _viewPurchaseOrder() { + return (Store store, dynamic dynamicAction, + NextDispatcher next) async { + final action = dynamicAction as ViewPurchaseOrder; + + next(action); + + store.dispatch(UpdateCurrentRoute(PurchaseOrderViewScreen.route)); + + if (store.state.prefState.isMobile) { + await navigatorKey.currentState.pushNamed(PurchaseOrderViewScreen.route); + } + }; +} + +Middleware _viewPurchaseOrderList() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ViewPurchaseOrderList; + + next(action); + + if (store.state.isStale) { + store.dispatch(RefreshData()); + } + + store.dispatch(UpdateCurrentRoute(PurchaseOrderScreen.route)); + + if (store.state.prefState.isMobile) { + navigatorKey.currentState.pushNamedAndRemoveUntil( + PurchaseOrderScreen.route, (Route route) => false); + } + }; +} + Middleware _editPurchaseOrder() { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as EditPurchaseOrder; @@ -54,36 +114,37 @@ Middleware _editPurchaseOrder() { }; } -Middleware _viewPurchaseOrder() { +Middleware _showEmailPurchaseOrder() { return (Store store, dynamic dynamicAction, NextDispatcher next) async { - final action = dynamicAction as ViewPurchaseOrder; + final action = dynamicAction as ShowEmailPurchaseOrder; next(action); - store.dispatch(UpdateCurrentRoute(PurchaseOrderViewScreen.route)); + store.dispatch(UpdateCurrentRoute(PurchaseOrderEmailScreen.route)); if (store.state.prefState.isMobile) { - navigatorKey.currentState.pushNamed(PurchaseOrderViewScreen.route); + final emailWasSent = await navigatorKey.currentState + .pushNamed(PurchaseOrderEmailScreen.route); + + if (action.completer != null && emailWasSent != null && emailWasSent) { + action.completer.complete(null); + } } }; } -Middleware _viewPurchaseOrderList() { - return (Store store, dynamic dynamicAction, NextDispatcher next) { - final action = dynamicAction as ViewPurchaseOrderList; +Middleware _showPdfPurchaseOrder() { + return (Store store, dynamic dynamicAction, + NextDispatcher next) async { + final action = dynamicAction as ShowPdfPurchaseOrder; next(action); - if (store.state.staticState.isStale) { - store.dispatch(RefreshData()); - } - - store.dispatch(UpdateCurrentRoute(PurchaseOrderScreen.route)); + store.dispatch(UpdateCurrentRoute(PurchaseOrderPdfScreen.route)); if (store.state.prefState.isMobile) { - navigatorKey.currentState.pushNamedAndRemoveUntil( - PurchaseOrderScreen.route, (Route route) => false); + navigatorKey.currentState.pushNamed(PurchaseOrderPdfScreen.route); } }; } @@ -120,6 +181,7 @@ Middleware _deletePurchaseOrder(PurchaseOrderRepository repository) { final prevPurchaseOrders = action.purchaseOrderIds .map((id) => store.state.purchaseOrderState.map[id]) .toList(); + repository .bulkAction(store.state.credentials, action.purchaseOrderIds, EntityAction.delete) @@ -146,6 +208,7 @@ Middleware _restorePurchaseOrder(PurchaseOrderRepository repository) { final prevPurchaseOrders = action.purchaseOrderIds .map((id) => store.state.purchaseOrderState.map[id]) .toList(); + repository .bulkAction(store.state.credentials, action.purchaseOrderIds, EntityAction.restore) @@ -166,18 +229,96 @@ Middleware _restorePurchaseOrder(PurchaseOrderRepository repository) { }; } +Middleware _approvePurchaseOrder(PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ApprovePurchaseOrders; + repository + .bulkAction(store.state.credentials, action.purchaseOrderIds, + EntityAction.approve) + .then((purchaseOrders) { + store.dispatch( + ApprovePurchaseOrderSuccess(purchaseOrders: purchaseOrders)); + store.dispatch(RefreshData()); + action.completer.complete(null); + }).catchError((Object error) { + print(error); + store.dispatch(ApprovePurchaseOrderFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _markSentPurchaseOrder( + PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as MarkPurchaseOrdersSentRequest; + repository + .bulkAction(store.state.credentials, action.purchaseOrderIds, + EntityAction.markSent) + .then((purchaseOrders) { + store.dispatch(MarkPurchaseOrderSentSuccess(purchaseOrders)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(MarkPurchaseOrderSentFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _emailPurchaseOrder(PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as EmailPurchaseOrderRequest; + final origPurchaseOrder = + store.state.purchaseOrderState.map[action.purchaseOrderId]; + repository + .emailPurchaseOrder(store.state.credentials, origPurchaseOrder, + action.template, action.subject, action.body) + .then((purchaseOrder) { + store.dispatch(EmailPurchaseOrderSuccess(purchaseOrder)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(EmailPurchaseOrderFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + Middleware _savePurchaseOrder(PurchaseOrderRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as SavePurchaseOrderRequest; + + // remove any empty line items + final updatedPurchaseOrder = action.purchaseOrder.rebuild((b) => b + ..lineItems.replace( + action.purchaseOrder.lineItems.where((item) => !item.isEmpty))); + repository - .saveData(store.state.credentials, action.purchaseOrder) + .saveData(store.state.credentials, updatedPurchaseOrder, action.action) .then((InvoiceEntity purchaseOrder) { if (action.purchaseOrder.isNew) { store.dispatch(AddPurchaseOrderSuccess(purchaseOrder)); } else { store.dispatch(SavePurchaseOrderSuccess(purchaseOrder)); } - + if (action.action == EntityAction.convertToInvoice) { + store.dispatch(RefreshData()); + } action.completer.complete(purchaseOrder); }).catchError((Object error) { print(error); @@ -192,11 +333,10 @@ Middleware _savePurchaseOrder(PurchaseOrderRepository repository) { Middleware _loadPurchaseOrder(PurchaseOrderRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as LoadPurchaseOrder; - final AppState state = store.state; store.dispatch(LoadPurchaseOrderRequest()); repository - .loadItem(state.credentials, action.purchaseOrderId) + .loadItem(store.state.credentials, action.purchaseOrderId) .then((purchaseOrder) { store.dispatch(LoadPurchaseOrderSuccess(purchaseOrder)); @@ -215,23 +355,82 @@ Middleware _loadPurchaseOrder(PurchaseOrderRepository repository) { }; } -Middleware _loadPurchaseOrders(PurchaseOrderRepository repository) { +Middleware _downloadPurchaseOrders( + PurchaseOrderRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { - final action = dynamicAction as LoadPurchaseOrders; - final AppState state = store.state; - - store.dispatch(LoadPurchaseOrdersRequest()); - repository.loadList(state.credentials).then((data) { - store.dispatch(LoadPurchaseOrdersSuccess(data)); - + final action = dynamicAction as DownloadPurchaseOrdersRequest; + repository + .bulkAction(store.state.credentials, action.invoiceIds, + EntityAction.bulkDownload) + .then((invoices) { + store.dispatch(DownloadPurchaseOrdersSuccess()); if (action.completer != null) { action.completer.complete(null); } - /* - if (state.productState.isStale) { - store.dispatch(LoadProducts()); + }).catchError((Object error) { + print(error); + store.dispatch(DownloadPurchaseOrdersFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _bulkEmailPurchaseOrders( + PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as BulkEmailPurchaseOrdersRequest; + + repository + .bulkAction(store.state.credentials, action.purchaseOrderIds, + EntityAction.emailPurchaseOrder) + .then((List purchaseOrders) { + store.dispatch(BulkEmailPurchaseOrdersSuccess(purchaseOrders)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(BulkEmailPurchaseOrdersFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadPurchaseOrders(PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadPurchaseOrders; + final state = store.state; + + store.dispatch(LoadPurchaseOrdersRequest()); + repository + .loadList( + state.credentials, + action.page, + state.createdAtLimit, + state.filterDeletedClients, + state.recordsPerPage, + ) + .then((data) { + store.dispatch(LoadPurchaseOrdersSuccess(data)); + if (data.length == state.recordsPerPage) { + store.dispatch(LoadPurchaseOrders( + completer: action.completer, + page: action.page + 1, + )); + } else { + if (action.completer != null) { + action.completer.complete(null); + } + store.dispatch(LoadCredits()); } - */ }).catchError((Object error) { print(error); store.dispatch(LoadPurchaseOrdersFailure(error)); @@ -243,3 +442,28 @@ Middleware _loadPurchaseOrders(PurchaseOrderRepository repository) { next(action); }; } + +Middleware _saveDocument(PurchaseOrderRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as SavePurchaseOrderDocumentRequest; + if (store.state.isEnterprisePlan) { + repository + .uploadDocument(store.state.credentials, action.purchaseOrder, + action.multipartFile) + .then((purchaseOrder) { + store.dispatch(SavePurchaseOrderSuccess(purchaseOrder)); + action.completer.complete(null); + }).catchError((Object error) { + print(error); + store.dispatch(SavePurchaseOrderDocumentFailure(error)); + action.completer.completeError(error); + }); + } else { + const error = 'Uploading documents requires an enterprise plan'; + store.dispatch(SavePurchaseOrderDocumentFailure(error)); + action.completer.completeError(error); + } + + next(action); + }; +}