From 7c7e50b78e82d374833508e2a754700c0d710b5d Mon Sep 17 00:00:00 2001 From: Rustem Date: Sun, 7 Jun 2026 14:40:31 +0500 Subject: [PATCH] fortenew commit prod --- lib/core/locator.dart | 3 + lib/core/models/check_item.dart | 9 +- lib/core/models/nct_product.dart | 53 +++++++++ lib/core/models/product_dao.dart | 5 +- lib/core/services/ApiService.dart | 5 +- lib/core/services/DataService.dart | 4 +- lib/core/services/NctService.dart | 75 ++++++++++++ lib/redux/actions/kassa_actions.dart | 8 +- lib/views/home/tabs/KassaTab.dart | 25 +++- .../tabs/kassaView/ProductAddBottomSheet.dart | 107 ++++++++++++++++-- lib/views/payment/payment_view.dart | 38 ++++++- 11 files changed, 303 insertions(+), 29 deletions(-) create mode 100644 lib/core/models/nct_product.dart create mode 100644 lib/core/services/NctService.dart diff --git a/lib/core/locator.dart b/lib/core/locator.dart index 7dd56ab..eaa0d39 100644 --- a/lib/core/locator.dart +++ b/lib/core/locator.dart @@ -3,6 +3,7 @@ import 'package:aman_kassa_flutter/core/services/BankService.dart' as bank; import 'package:aman_kassa_flutter/core/services/ForteService.dart' as forte; import 'package:aman_kassa_flutter/core/services/DataService.dart'; +import 'package:aman_kassa_flutter/core/services/NctService.dart'; import 'package:aman_kassa_flutter/core/services/blue_print_service.dart'; import '../core/services/DbService.dart'; @@ -35,6 +36,8 @@ class LocatorInjector { // depencies + _log.d('Initializing NctService Service'); + locator.registerLazySingleton(() => NctService()); _log.d('Initializing DataService Service'); locator.registerLazySingleton(() => DataService()); _log.d('Initializing BankService Service'); diff --git a/lib/core/models/check_item.dart b/lib/core/models/check_item.dart index 2159585..afa42e7 100644 --- a/lib/core/models/check_item.dart +++ b/lib/core/models/check_item.dart @@ -4,7 +4,9 @@ class CheckItem { final num price; final int articul; final String? excise; - CheckItem({required this.name, required this.cnt, required this.price, required this.articul, this.excise}); + final String? ntin; + + CheckItem({required this.name, required this.cnt, required this.price, required this.articul, this.excise, this.ntin}); static CheckItem fromJson(Map json) { return CheckItem( @@ -13,14 +15,17 @@ class CheckItem { price: json['price'], articul: json['articul'], excise: json['excise'], + ntin: json['ntin'], ); } + Map toJson() => { 'name': name, 'cnt': cnt, 'price': price, 'articul': articul, - 'excise' : excise + 'excise': excise, + 'ntin': ntin ?? '', }; } \ No newline at end of file diff --git a/lib/core/models/nct_product.dart b/lib/core/models/nct_product.dart new file mode 100644 index 0000000..212b49a --- /dev/null +++ b/lib/core/models/nct_product.dart @@ -0,0 +1,53 @@ +class NctProduct { + final int? id; + final String? nameRu; + final String? nameKk; + final String? gtin; + final String? ntinCode; + final bool? isSocial; + final int? status; + final bool? isMarkedEac; + final String? measure; + final bool? ntinIsDeactivated; + final String? ntinDeactivationReason; + final String? ntinDuplicateOfProduct; + + NctProduct({ + this.id, + this.nameRu, + this.nameKk, + this.gtin, + this.ntinCode, + this.isSocial, + this.status, + this.isMarkedEac, + this.measure, + this.ntinIsDeactivated, + this.ntinDeactivationReason, + this.ntinDuplicateOfProduct, + }); + + factory NctProduct.fromJson(Map json) { + return NctProduct( + id: json['id'] is int ? json['id'] as int : null, + nameRu: json['name_ru'] is String ? json['name_ru'] as String : null, + nameKk: json['name_kk'] is String ? json['name_kk'] as String : null, + gtin: json['gtin'] is String ? json['gtin'] as String : null, + ntinCode: json['ntin_code'] is String ? json['ntin_code'] as String : null, + isSocial: json['is_social'] is bool ? json['is_social'] as bool : null, + status: json['status'] is int ? json['status'] as int : null, + isMarkedEac: json['is_markedeac'] is bool ? json['is_markedeac'] as bool : null, + measure: _parseStringOrObject(json['measure']), + ntinIsDeactivated: json['ntin_isdeactivated'] is bool ? json['ntin_isdeactivated'] as bool : null, + ntinDeactivationReason: _parseStringOrObject(json['ntin_deactivationreason']), + ntinDuplicateOfProduct: _parseStringOrObject(json['ntin_duplicateofproduct']), + ); + } + + static String? _parseStringOrObject(dynamic value) { + if (value == null) return null; + if (value is String) return value; + if (value is Map) return value['name_ru']?.toString() ?? value['name']?.toString() ?? value.toString(); + return value.toString(); + } +} diff --git a/lib/core/models/product_dao.dart b/lib/core/models/product_dao.dart index 6400aee..9df4698 100644 --- a/lib/core/models/product_dao.dart +++ b/lib/core/models/product_dao.dart @@ -9,8 +9,7 @@ class ProductDao { final Good? good; final Service? service; final String? excise; + final String? ntin; - - ProductDao( {required this.name, required this.price, required this.count, required this.total, this.good, this.service, this.excise }); - + ProductDao({required this.name, required this.price, required this.count, required this.total, this.good, this.service, this.excise, this.ntin}); } \ No newline at end of file diff --git a/lib/core/services/ApiService.dart b/lib/core/services/ApiService.dart index fed483b..b560dd7 100644 --- a/lib/core/services/ApiService.dart +++ b/lib/core/services/ApiService.dart @@ -147,8 +147,11 @@ class ApiService extends BaseService { return Response.fromJsonDynamic(json.decode(response)); } - Future> sellReturn(String token, String checkData) async { + Future> sellReturn(String token, String checkData, {String? ticketNumber}) async { Map requestBody = {'api_token': token, 'data': checkData}; + if (ticketNumber != null && ticketNumber.isNotEmpty) { + requestBody['ticketNumber'] = ticketNumber; + } var response = await requestFormData('/sell_return', requestBody); return Response.fromJsonDynamic(json.decode(response)); } diff --git a/lib/core/services/DataService.dart b/lib/core/services/DataService.dart index 192ea1f..e922e29 100644 --- a/lib/core/services/DataService.dart +++ b/lib/core/services/DataService.dart @@ -71,6 +71,7 @@ class DataService extends BaseService { price: el.price, articul: articul, excise: el.excise, + ntin: el.ntin, )); summ += el.total; iterator++; @@ -191,6 +192,7 @@ class DataService extends BaseService { {String? paymentType, String? tradeType, String? contragent, + String? ticketNumber, required String token, required List kassaItems, required List calcItems, @@ -217,7 +219,7 @@ class DataService extends BaseService { // log.i('data: $data'); Response response = await (operationType == OperationTypePay ? _api.sell(token, data) - : _api.sellReturn(token, data)); + : _api.sellReturn(token, data, ticketNumber: ticketNumber)); // log.i('response status: ${response.status}'); // log.i('response operation: ${response.operation}'); if (response.status == 200 && response.operation == true) { diff --git a/lib/core/services/NctService.dart b/lib/core/services/NctService.dart new file mode 100644 index 0000000..3217d66 --- /dev/null +++ b/lib/core/services/NctService.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:aman_kassa_flutter/core/base/base_service.dart'; +import 'package:aman_kassa_flutter/core/models/nct_product.dart'; +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +class NctService extends BaseService { + static const String _baseUrl = 'https://nct.gov.kz/api/integration/ofd/search_ofd/'; + + Future> searchByNtin(String ntin) async { + final uri = Uri.parse(_baseUrl).replace(queryParameters: {'tin': ntin}); + log.i('NCT request: $uri'); + + http.Client client = _buildClient(); + late http.Response response; + try { + response = await client.get(uri, headers: {'Accept': 'application/json'}); + } on TlsException catch (e) { + log.e('NCT TLS error', e); + throw NctServiceException('Ошибка SSL при подключении к NCT: ${e.message}'); + } on SocketException catch (e) { + log.e('NCT network error', e); + throw NctServiceException('Ошибка сети при подключении к NCT: ${e.message}'); + } catch (e) { + log.e('NCT request error', e); + throw NctServiceException('Ошибка запроса к NCT: $e'); + } finally { + client.close(); + } + + log.i('NCT status: ${response.statusCode}'); + + if (response.statusCode != 200) { + throw NctServiceException('NCT вернул статус ${response.statusCode}'); + } + + try { + final bodyString = utf8.decode(response.bodyBytes); + log.i('NCT body: ${bodyString.length > 300 ? bodyString.substring(0, 300) : bodyString}'); + final decoded = json.decode(bodyString); + List list; + if (decoded is List) { + list = decoded; + } else if (decoded is Map && decoded.containsKey('results')) { + list = decoded['results'] as List; + } else { + log.w('NCT unexpected response format: ${decoded.runtimeType}'); + return []; + } + return list + .map((e) => NctProduct.fromJson(e as Map)) + .toList(); + } catch (e) { + log.e('NCT JSON parse error', e); + throw NctServiceException('Ошибка разбора ответа NCT: $e'); + } + } + + http.Client _buildClient() { + final httpClient = HttpClient() + ..badCertificateCallback = (cert, host, port) { + log.w('NCT bad certificate for $host:$port — subject: ${cert.subject}'); + return host == 'nct.gov.kz'; + }; + return IOClient(httpClient); + } +} + +class NctServiceException implements Exception { + final String message; + NctServiceException(this.message); + @override + String toString() => message; +} diff --git a/lib/redux/actions/kassa_actions.dart b/lib/redux/actions/kassa_actions.dart index f0e262c..23f8961 100644 --- a/lib/redux/actions/kassa_actions.dart +++ b/lib/redux/actions/kassa_actions.dart @@ -36,12 +36,12 @@ Future cleanKassaItems(Store store) async { store.dispatch(SetKassaStateAction(KassaState(kassaItems: []))); } -ThunkAction addCustomProductToKassaItems(String name, double count, double price, double total) { +ThunkAction addCustomProductToKassaItems(String name, double count, double price, double total, {String? ntin}) { return (Store store) async { List items = store.state.kassaState!.kassaItems!; - items.add(new ProductDao(name: name, count: count, price: price, total: total)); - store.dispatch(SetKassaStateAction(KassaState(kassaItems: items))); - }; + items.add(new ProductDao(name: name, count: count, price: price, total: total, ntin: ntin)); + store.dispatch(SetKassaStateAction(KassaState(kassaItems: items))); + }; } diff --git a/lib/views/home/tabs/KassaTab.dart b/lib/views/home/tabs/KassaTab.dart index f2feae3..db2b787 100644 --- a/lib/views/home/tabs/KassaTab.dart +++ b/lib/views/home/tabs/KassaTab.dart @@ -1,8 +1,10 @@ import 'package:aman_kassa_flutter/core/entity/Goods.dart'; import 'package:aman_kassa_flutter/core/locator.dart'; +import 'package:aman_kassa_flutter/core/models/nct_product.dart'; import 'package:aman_kassa_flutter/core/models/product_dao.dart'; import 'package:aman_kassa_flutter/core/route_names.dart'; import 'package:aman_kassa_flutter/core/services/DataService.dart'; +import 'package:aman_kassa_flutter/core/services/NctService.dart' show NctService, NctServiceException; import 'package:aman_kassa_flutter/core/services/dialog_service.dart'; import 'package:aman_kassa_flutter/core/services/navigator_service.dart'; import 'package:aman_kassa_flutter/redux/actions/kassa_actions.dart'; @@ -30,6 +32,7 @@ class KassaTab extends StatelessWidget { final NavigatorService _navigatorService = locator(); final DialogService _dialogService = locator(); final DataService _dataService = locator(); + final NctService _nctService = locator(); final int index; @@ -221,8 +224,20 @@ class KassaTab extends StatelessWidget { if (goods != null && goods.isNotEmpty) { await Redux.store!.dispatch(addProductToKassaItems(goods.first, dataMatrix)); } else { - _dialogService.showDialog( - description: 'Товар не найден: $barcode'); + try { + List nctResults = await _nctService.searchByNtin(barcode); + if (nctResults.isNotEmpty) { + final NctProduct nctProduct = nctResults.first; + final String? name = nctProduct.nameRu ?? nctProduct.nameKk; + _openAddSheetWithName(name, ntinCode: nctProduct.ntinCode); + } else { + _dialogService.showDialog(description: 'Товар не найден: $barcode'); + } + } on NctServiceException catch (e) { + _dialogService.showDialog(description: e.message); + } catch (e) { + _dialogService.showDialog(description: 'Ошибка поиска NCT: $e'); + } } } else if (result.type == ResultType.Error) { _dialogService.showDialog(description: 'Не верный формат QR кода'); @@ -247,6 +262,12 @@ class KassaTab extends StatelessWidget { + void _openAddSheetWithName(String? name, {String? ntinCode}) { + _navigatorService.navigateToPage( + MaterialPageRoute(builder: (_) => ProductAddBottomSheet(initialName: name, initialNtinCode: ntinCode)) + ); + } + void showModalBottomSheetCatalog(BuildContext context, String action) { if (action == 'add') { diff --git a/lib/views/home/tabs/kassaView/ProductAddBottomSheet.dart b/lib/views/home/tabs/kassaView/ProductAddBottomSheet.dart index 8dfcf5a..496cde8 100644 --- a/lib/views/home/tabs/kassaView/ProductAddBottomSheet.dart +++ b/lib/views/home/tabs/kassaView/ProductAddBottomSheet.dart @@ -1,3 +1,6 @@ +import 'package:aman_kassa_flutter/core/locator.dart'; +import 'package:aman_kassa_flutter/core/models/nct_product.dart'; +import 'package:aman_kassa_flutter/core/services/NctService.dart' show NctService, NctServiceException; import 'package:aman_kassa_flutter/redux/actions/kassa_actions.dart'; import 'package:aman_kassa_flutter/redux/store.dart'; import 'package:aman_kassa_flutter/shared/app_colors.dart'; @@ -7,29 +10,39 @@ import 'package:flutter/services.dart'; class ProductAddBottomSheet extends StatefulWidget { final ScrollController? scrollController; + final String? initialName; + final String? initialNtinCode; - ProductAddBottomSheet({this.scrollController}); + ProductAddBottomSheet({this.scrollController, this.initialName, this.initialNtinCode}); @override _ProductAddBottomSheetState createState() => _ProductAddBottomSheetState(); } class _ProductAddBottomSheetState extends State { + final NctService _nctService = locator(); + + late TextEditingController ntinController; late TextEditingController nameController; late TextEditingController countController; late TextEditingController priceController; double sum = 0.0; + bool _isSearching = false; + String? _foundNtinCode; @override void initState() { super.initState(); - nameController = new TextEditingController(); + ntinController = new TextEditingController(); + nameController = new TextEditingController(text: widget.initialName ?? ''); countController = new TextEditingController(); priceController = new TextEditingController(); + _foundNtinCode = widget.initialNtinCode; } @override void dispose() { + ntinController.dispose(); nameController.dispose(); countController.dispose(); priceController.dispose(); @@ -51,10 +64,48 @@ class _ProductAddBottomSheetState extends State { ), ), body: Padding( - padding: EdgeInsets.only(top: 15, left: 10, right: 15, bottom: 0 ), + padding: EdgeInsets.only(top: 15, left: 10, right: 15, bottom: 0), child: ListView( controller: widget.scrollController, children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextField( + decoration: new InputDecoration( + border: new OutlineInputBorder( + borderSide: new BorderSide(color: primaryColor)), + hintText: 'Введите NTIN/GTIN', + labelText: 'NTIN/GTIN', + prefixText: ' ', + ), + keyboardType: TextInputType.text, + controller: ntinController, + ), + ), + SizedBox(width: 8), + SizedBox( + height: 56, + child: ElevatedButton( + onPressed: _isSearching ? null : _searchByNtin, + style: ElevatedButton.styleFrom( + primary: primaryColor, + padding: EdgeInsets.symmetric(horizontal: 12), + ), + child: _isSearching + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2), + ) + : Icon(Icons.search, color: Colors.white), + ), + ), + ], + ), + verticalSpaceSmall, TextField( decoration: new InputDecoration( border: new OutlineInputBorder( @@ -78,8 +129,6 @@ class _ProductAddBottomSheetState extends State { decimal: true, ), inputFormatters: [ - // WhitelistingTextInputFormatter.digitsOnly - // FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.allow(RegExp("^[0-9.]*")), ], controller: countController, @@ -152,17 +201,52 @@ class _ProductAddBottomSheetState extends State { ); } + Future _searchByNtin() async { + final ntin = ntinController.text.trim(); + if (ntin.isEmpty) { + _showErrorDialog('Введите NTIN/GTIN для поиска.'); + return; + } + setState(() => _isSearching = true); + try { + List results = await _nctService.searchByNtin(ntin); + if (results.isNotEmpty) { + final product = results.first; + setState(() { + nameController.text = product.nameRu ?? product.nameKk ?? ''; + _foundNtinCode = product.ntinCode; + }); + if (product.ntinIsDeactivated == true) { + String reason = product.ntinDeactivationReason ?? ''; + String duplicate = product.ntinDuplicateOfProduct != null + ? '\nДубликат: ${product.ntinDuplicateOfProduct}' + : ''; + _showErrorDialog('Карточка деактивирована.$reason$duplicate'); + } + } else { + _showErrorDialog('Товар с NTIN/GTIN "$ntin" не найден.'); + } + } on NctServiceException catch (e) { + _showErrorDialog(e.message); + } catch (e) { + _showErrorDialog('Ошибка поиска NCT: $e'); + } finally { + setState(() => _isSearching = false); + } + } + void submit() { if (nameController.text.isEmpty || countController.text.isEmpty || priceController.text.isEmpty) { - _showDialog(); + _showErrorDialog('Введите наименование, количество и цену'); } else { Redux.store!.dispatch(addCustomProductToKassaItems( nameController.text, double.parse(countController.text), double.parse(priceController.text), - sum)); + sum, + ntin: _foundNtinCode)); Navigator.pop(context); } } @@ -185,18 +269,17 @@ class _ProductAddBottomSheetState extends State { } } - void _showDialog() { - // flutter defined function + void _showErrorDialog(String message) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: new Text("Aman Касса"), - content: new Text("Введите наименование, количество и цену"), + content: new Text(message), actions: [ - FlatButton( + TextButton( child: Text( - "ОK", + "ОК", style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), onPressed: () { diff --git a/lib/views/payment/payment_view.dart b/lib/views/payment/payment_view.dart index 23c37f7..938654a 100644 --- a/lib/views/payment/payment_view.dart +++ b/lib/views/payment/payment_view.dart @@ -56,6 +56,7 @@ class _PaymentViewState extends State { dynamic _bankService; final NavigatorService _navigatorService = locator(); final TextEditingController _iinController = new TextEditingController(); + final TextEditingController _ticketNumberController = new TextEditingController(); late bool isBusy; late bool isBankApiAccess; @@ -72,6 +73,7 @@ class _PaymentViewState extends State { @override void dispose() { _iinController.dispose(); + _ticketNumberController.dispose(); super.dispose(); } @@ -177,6 +179,24 @@ class _PaymentViewState extends State { }); } + bool get _isReturn => widget.model.operationType == OperationTypeReturn; + + bool _canProceed() { + if (_isReturn && _ticketNumberController.text.trim().isEmpty) { + return false; + } + return true; + } + + void _onPaymentPressed(String type, CardData? cardData) { + if (!_canProceed()) { + _dialogService.showDialog( + description: 'Введите фискальный признак для выполнения возврата.'); + return; + } + pressPayment(type, cardData); + } + Expanded _buildBodyContent() { return Expanded( child: Column( @@ -188,6 +208,15 @@ class _PaymentViewState extends State { decoration: InputDecoration( labelText: 'ИИН Покупателя', hintText: "Введите ИИН покупателя"), ), + if (_isReturn) + TextField( + keyboardType: TextInputType.text, + controller: _ticketNumberController, + decoration: InputDecoration( + labelText: 'Фискальный признак *', + hintText: 'Введите фискальный признак'), + ), + if (_isReturn) verticalSpaceSmall, Container( height: 150, child: Row( @@ -197,7 +226,7 @@ class _PaymentViewState extends State { child: BusyButton( title: 'Оплатить картой', onPressed: () { - pressPayment('card', null); + _onPaymentPressed('card', null); }, mainColor: primaryColor, )), @@ -206,7 +235,7 @@ class _PaymentViewState extends State { child: BusyButton( title: 'Мобильный', onPressed: () { - pressPayment('mobile', null); + _onPaymentPressed('mobile', null); }, mainColor: redColor, )), @@ -215,7 +244,7 @@ class _PaymentViewState extends State { child: BusyButton( title: 'Наличными', onPressed: () { - pressPayment('cash', null); + _onPaymentPressed('cash', null); }, mainColor: greenColor, )), @@ -360,7 +389,8 @@ class _PaymentViewState extends State { calcItems: calcItems, mode: _mode, cardData: cardData, - contragent: _iinController.text + contragent: _iinController.text, + ticketNumber: _isReturn ? _ticketNumberController.text.trim() : null, ); setState(() { isBusy = false;