fortenew commit prod

fortenew
Rustem 2026-06-07 14:40:31 +05:00
parent 1a39e1eed4
commit 7c7e50b78e
11 changed files with 303 additions and 29 deletions

View File

@ -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>(() => NctService());
_log.d('Initializing DataService Service');
locator.registerLazySingleton<DataService>(() => DataService());
_log.d('Initializing BankService Service');

View File

@ -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<String, dynamic> json) {
return CheckItem(
@ -13,14 +15,17 @@ class CheckItem {
price: json['price'],
articul: json['articul'],
excise: json['excise'],
ntin: json['ntin'],
);
}
Map<String, dynamic> toJson() =>
{
'name': name,
'cnt': cnt,
'price': price,
'articul': articul,
'excise' : excise
'excise': excise,
'ntin': ntin ?? '',
};
}

View File

@ -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<String, dynamic> 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();
}
}

View File

@ -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});
}

View File

@ -147,8 +147,11 @@ class ApiService extends BaseService {
return Response.fromJsonDynamic(json.decode(response));
}
Future<Response<dynamic>> sellReturn(String token, String checkData) async {
Future<Response<dynamic>> sellReturn(String token, String checkData, {String? ticketNumber}) async {
Map<String, String> requestBody = <String, String>{'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));
}

View File

@ -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<ProductDao> kassaItems,
required List<CalcModel> calcItems,
@ -217,7 +219,7 @@ class DataService extends BaseService {
// log.i('data: $data');
Response<dynamic> 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) {

View File

@ -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<List<NctProduct>> 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<dynamic> list;
if (decoded is List) {
list = decoded;
} else if (decoded is Map && decoded.containsKey('results')) {
list = decoded['results'] as List<dynamic>;
} else {
log.w('NCT unexpected response format: ${decoded.runtimeType}');
return [];
}
return list
.map((e) => NctProduct.fromJson(e as Map<String, dynamic>))
.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;
}

View File

@ -36,12 +36,12 @@ Future<void> cleanKassaItems(Store<AppState> store) async {
store.dispatch(SetKassaStateAction(KassaState(kassaItems: [])));
}
ThunkAction<AppState> addCustomProductToKassaItems(String name, double count, double price, double total) {
ThunkAction<AppState> addCustomProductToKassaItems(String name, double count, double price, double total, {String? ntin}) {
return (Store<AppState> store) async {
List<ProductDao> 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)));
};
}

View File

@ -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<NavigatorService>();
final DialogService _dialogService = locator<DialogService>();
final DataService _dataService = locator<DataService>();
final NctService _nctService = locator<NctService>();
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<NctProduct> 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') {

View File

@ -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<ProductAddBottomSheet> {
final NctService _nctService = locator<NctService>();
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<ProductAddBottomSheet> {
),
),
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: <Widget>[
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<ProductAddBottomSheet> {
decimal: true,
),
inputFormatters: <TextInputFormatter>[
// WhitelistingTextInputFormatter.digitsOnly
// FilteringTextInputFormatter.digitsOnly
FilteringTextInputFormatter.allow(RegExp("^[0-9.]*")),
],
controller: countController,
@ -152,17 +201,52 @@ class _ProductAddBottomSheetState extends State<ProductAddBottomSheet> {
);
}
Future<void> _searchByNtin() async {
final ntin = ntinController.text.trim();
if (ntin.isEmpty) {
_showErrorDialog('Введите NTIN/GTIN для поиска.');
return;
}
setState(() => _isSearching = true);
try {
List<NctProduct> 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<ProductAddBottomSheet> {
}
}
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: <Widget>[
FlatButton(
TextButton(
child: Text(
"ОK",
"ОК",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
onPressed: () {

View File

@ -56,6 +56,7 @@ class _PaymentViewState extends State<PaymentView> {
dynamic _bankService;
final NavigatorService _navigatorService = locator<NavigatorService>();
final TextEditingController _iinController = new TextEditingController();
final TextEditingController _ticketNumberController = new TextEditingController();
late bool isBusy;
late bool isBankApiAccess;
@ -72,6 +73,7 @@ class _PaymentViewState extends State<PaymentView> {
@override
void dispose() {
_iinController.dispose();
_ticketNumberController.dispose();
super.dispose();
}
@ -177,6 +179,24 @@ class _PaymentViewState extends State<PaymentView> {
});
}
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<PaymentView> {
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<PaymentView> {
child: BusyButton(
title: 'Оплатить картой',
onPressed: () {
pressPayment('card', null);
_onPaymentPressed('card', null);
},
mainColor: primaryColor,
)),
@ -206,7 +235,7 @@ class _PaymentViewState extends State<PaymentView> {
child: BusyButton(
title: 'Мобильный',
onPressed: () {
pressPayment('mobile', null);
_onPaymentPressed('mobile', null);
},
mainColor: redColor,
)),
@ -215,7 +244,7 @@ class _PaymentViewState extends State<PaymentView> {
child: BusyButton(
title: 'Наличными',
onPressed: () {
pressPayment('cash', null);
_onPaymentPressed('cash', null);
},
mainColor: greenColor,
)),
@ -360,7 +389,8 @@ class _PaymentViewState extends State<PaymentView> {
calcItems: calcItems,
mode: _mode,
cardData: cardData,
contragent: _iinController.text
contragent: _iinController.text,
ticketNumber: _isReturn ? _ticketNumberController.text.trim() : null,
);
setState(() {
isBusy = false;