Add full app source
Adds the Flutter inventory app source, cleaned up for a shared repo: generic package name, no hardcoded ERP endpoints (moved to gitignored local config), no dead auth code, no debug logging of session data.
This commit is contained in:
640
lib/service/axelor_client.dart
Normal file
640
lib/service/axelor_client.dart
Normal file
@@ -0,0 +1,640 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:inventory_app/constants.dart';
|
||||
import 'package:inventory_app/models/famille_produit.dart';
|
||||
import 'package:inventory_app/models/depot.dart';
|
||||
import 'package:inventory_app/models/inventory_line.dart';
|
||||
import 'package:inventory_app/models/product.dart';
|
||||
import 'package:inventory_app/models/tracking_number.dart';
|
||||
import 'package:inventory_app/utils.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class AxelorClient {
|
||||
late Dio dio;
|
||||
late PersistCookieJar cookieJar;
|
||||
|
||||
static final GetStorage _storage = GetStorage();
|
||||
|
||||
AxelorClient._internal();
|
||||
|
||||
static Future<AxelorClient> create() async {
|
||||
final client = AxelorClient._internal();
|
||||
|
||||
Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
String cookiePath = '${appDocDir.path}/cookies';
|
||||
|
||||
client.cookieJar = PersistCookieJar(storage: FileStorage(cookiePath));
|
||||
client.dio = Dio(BaseOptions(baseUrl: Utils.url));
|
||||
client.dio.interceptors.add(CookieManager(client.cookieJar));
|
||||
|
||||
// Set saved session ID if available
|
||||
final storedSessionId = _storage.read('sessionId');
|
||||
if (storedSessionId != null) {
|
||||
client.dio.options.headers['Cookie'] = 'JSESSIONID=$storedSessionId';
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<bool> login(String username, String password) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/login.jsp',
|
||||
data: {'username': username, 'password': password},
|
||||
options: Options(
|
||||
contentType: Headers.formUrlEncodedContentType,
|
||||
followRedirects: false,
|
||||
// Make sure redirect is not automatically followed
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Android; Flutter App)',
|
||||
'Accept': '*/*',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Handle 302 redirect: cookies should be in headers
|
||||
final cookies = response.headers['set-cookie'];
|
||||
if (cookies != null) {
|
||||
// Parse JSESSIONID
|
||||
String? sessionId;
|
||||
for (var cookie in cookies) {
|
||||
if (cookie.contains('JSESSIONID')) {
|
||||
sessionId = cookie.split(';').first.split('=').last;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionId != null && sessionId.isNotEmpty) {
|
||||
// Set the cookie so the validation request is authenticated
|
||||
dio.options.headers['Cookie'] = 'JSESSIONID=$sessionId';
|
||||
|
||||
// Verify the session is actually authenticated (not just an anonymous session)
|
||||
final valid = await isSessionValid();
|
||||
if (!valid) {
|
||||
dio.options.headers.remove('Cookie');
|
||||
return false;
|
||||
}
|
||||
|
||||
await _storage.write('sessionId', sessionId);
|
||||
await _storage.write('username', username);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> logout() async {
|
||||
try {
|
||||
final response = await dio.get('/logout');
|
||||
if (response.statusCode == 200) {
|
||||
await _storage.remove('sessionId');
|
||||
await _storage.remove('username');
|
||||
await cookieJar.deleteAll();
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<Depot>?> fetchLocations() async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'/ws/rest/com.axelor.apps.stock.db.StockLocation/search',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 800,
|
||||
"data": {
|
||||
"criteria": [
|
||||
{
|
||||
"fieldName": "usableOnImmobilisation",
|
||||
"operator": "=",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<Depot> depots = [];
|
||||
for (var depot in response.data["data"]) {
|
||||
depots.add(Depot.fromJson(depot));
|
||||
}
|
||||
return depots;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveInventoryLine({required InventoryLine inventoryLine}) async {
|
||||
final String url =
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine';
|
||||
String domain;
|
||||
|
||||
if (inventoryLine.trackingNumberId == null) {
|
||||
domain =
|
||||
"self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber IS NULL";
|
||||
} else {
|
||||
domain =
|
||||
"self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber.id = :trackingNumberId";
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Check if InventoryLine already exists
|
||||
final searchResponse = await dio.post(
|
||||
'$url/search',
|
||||
data: {
|
||||
"data": {
|
||||
"_domain": domain,
|
||||
"_domainContext": {
|
||||
"inventoryId": inventoryLine.inventoryId,
|
||||
"product": inventoryLine.productId,
|
||||
"trackingNumberId": inventoryLine.trackingNumberId,
|
||||
},
|
||||
"_archived": false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (searchResponse.statusCode == 200 &&
|
||||
searchResponse.data['status'] == 0) {
|
||||
final existing = searchResponse.data['data'];
|
||||
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
final existingLine = existing[0];
|
||||
inventoryLine.id = existingLine['id'];
|
||||
inventoryLine.version = existingLine['version'];
|
||||
|
||||
inventoryLine.firstCounting =
|
||||
inventoryLine.firstCounting != 0
|
||||
? inventoryLine.firstCounting
|
||||
: double.tryParse(
|
||||
existingLine['firstCounting']?.toString() ?? '',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
inventoryLine.secondCounting =
|
||||
inventoryLine.secondCounting != 0
|
||||
? inventoryLine.secondCounting
|
||||
: double.tryParse(
|
||||
existingLine['secondCounting']?.toString() ?? '',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
inventoryLine.thirdCounting =
|
||||
inventoryLine.thirdCounting != 0
|
||||
? inventoryLine.thirdCounting
|
||||
: double.tryParse(
|
||||
existingLine['thirdCounting']?.toString() ?? '',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
inventoryLine.firstCountingDate =
|
||||
inventoryLine.firstCountingDate ??
|
||||
existingLine['firstCountingDate'];
|
||||
|
||||
inventoryLine.secondCountingDate =
|
||||
inventoryLine.secondCountingDate ??
|
||||
existingLine['secondCountingDate'];
|
||||
|
||||
inventoryLine.thirdCountingDate =
|
||||
inventoryLine.thirdCountingDate ??
|
||||
existingLine['thirdCountingDate'];
|
||||
|
||||
inventoryLine.firstCountingByUser =
|
||||
inventoryLine.firstCountingByUser ??
|
||||
existingLine['firstCountingByUser'];
|
||||
|
||||
inventoryLine.secondCountingByUser =
|
||||
inventoryLine.secondCountingByUser ??
|
||||
existingLine['secondCountingByUser'];
|
||||
|
||||
inventoryLine.thirdCountingByUser =
|
||||
inventoryLine.thirdCountingByUser ??
|
||||
existingLine['thirdCountingByUser'];
|
||||
} else {}
|
||||
}
|
||||
|
||||
// 2. Create or update the InventoryLine
|
||||
final saveResponse = await dio.post(
|
||||
url,
|
||||
data: jsonEncode({"data": inventoryLine.toJson()}),
|
||||
);
|
||||
|
||||
if (saveResponse.statusCode == 200) {
|
||||
final result = saveResponse.data;
|
||||
if (result['status'] == 0) {
|
||||
} else {}
|
||||
} else {}
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
Future<Product?> fetchProductByCode(String code) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.base.db.Product/search',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 1,
|
||||
"sortBy": ["code", "name", "unit"],
|
||||
"data": {
|
||||
"criteria": [
|
||||
{"fieldName": "code", "operator": "=", "value": code},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['status'] == 0) {
|
||||
final list = response.data["data"];
|
||||
if (list != null && list.isNotEmpty) {
|
||||
return Product.fromJson(list[0]);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<TrackingNumber>?> fetchTrackingNumberByProduct(int id) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 100,
|
||||
"fields": ["trackingNumberSeq", "perishableExpirationDate"],
|
||||
"data": {
|
||||
"criteria": [
|
||||
{"fieldName": "product.id", "operator": "=", "value": id},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<TrackingNumber> trackingNumbers = [];
|
||||
for (var trackingNumber in response.data["data"]) {
|
||||
trackingNumbers.add(TrackingNumber.fromJson(trackingNumber));
|
||||
}
|
||||
return trackingNumbers;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<InventoryLine>?> fetchInventoryLinesByLocation(
|
||||
int locationId,
|
||||
) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search',
|
||||
data: {
|
||||
"data": {
|
||||
"_domain":
|
||||
"self.stockLocation.id = :locationId and self.inventory.id = :inventoryId",
|
||||
"_domainContext": {
|
||||
"locationId": locationId,
|
||||
"inventoryId": kInventoryId,
|
||||
},
|
||||
"_archived": false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<InventoryLine> lines = [];
|
||||
for (var line in response.data["data"]) {
|
||||
lines.add(InventoryLine.fromJson(line));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isSessionValid() async {
|
||||
try {
|
||||
final res = await dio.get(
|
||||
'/ws/rest/com.axelor.apps.stock.db.StockLocation?offset=0&limit=1',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 1,
|
||||
"data": {
|
||||
"criteria": [
|
||||
{
|
||||
"fieldName": "usableOnImmobilisation",
|
||||
"operator": "=",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
return res.statusCode == 200 && res.data['status'] == 0;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> fetchUserProfile() async {
|
||||
final username = _storage.read('username');
|
||||
if (username == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'/ws/rest/com.axelor.auth.db.User/search',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 1,
|
||||
"data": {
|
||||
"criteria": [
|
||||
{"fieldName": "code", "operator": "=", "value": username},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 &&
|
||||
response.data['status'] == 0 &&
|
||||
response.data['data'] != null &&
|
||||
response.data['data'].isNotEmpty) {
|
||||
final user = response.data['data'][0];
|
||||
return user;
|
||||
} else {}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> fetchStockLocationByName(String name) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.StockLocation/search',
|
||||
data: {
|
||||
"offset": 0,
|
||||
"limit": 10000,
|
||||
"data": {
|
||||
"criteria": [
|
||||
{"fieldName": "name", "operator": "=", "value": "$name"},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 &&
|
||||
response.data['status'] == 0 &&
|
||||
response.data['data'] != null &&
|
||||
response.data['data'].isNotEmpty) {
|
||||
final location = response.data['data'][0];
|
||||
return location;
|
||||
} else {}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List?> fetchImageBytes(int imageId) async {
|
||||
final url =
|
||||
'${Utils.url}/ws/rest/com.axelor.meta.db.MetaFile/$imageId/content/download';
|
||||
|
||||
try {
|
||||
final response = await dio.get(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
|
||||
final contentType = response.headers.value('content-type');
|
||||
|
||||
if (contentType == null ||
|
||||
(!contentType.startsWith('image/') &&
|
||||
contentType != 'application/octet-stream')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Uint8List.fromList(response.data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Product?> createProducts(Product product, Depot? depot) async {
|
||||
final String url = '${Utils.url}/ws/rest/com.axelor.apps.base.db.Product';
|
||||
|
||||
try {
|
||||
// Create or update the InventoryLine
|
||||
final saveResponse = await dio.post(
|
||||
url,
|
||||
data: jsonEncode({"data": product.toJson()}),
|
||||
);
|
||||
|
||||
if (saveResponse.statusCode == 200) {
|
||||
final result = saveResponse.data;
|
||||
if (result['status'] == 0) {
|
||||
Product newProduct = Product.fromJson(result["data"][0]);
|
||||
TrackingNumber? trackingNumber = await createTrackingNumber(
|
||||
trackingNumber: TrackingNumber(
|
||||
trackingNumberSeq: product.internalDescription,
|
||||
product: Product(id: newProduct.id),
|
||||
),
|
||||
);
|
||||
|
||||
// Create initial inventory line
|
||||
await createInitialInventoryLine(newProduct, trackingNumber, depot);
|
||||
|
||||
return newProduct;
|
||||
} else {}
|
||||
} else {}
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<FamilleProduit>?> getFamilleProduit() async {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search',
|
||||
data: {
|
||||
"data": {
|
||||
"criteria": [
|
||||
{
|
||||
"operator": "and",
|
||||
"criteria": [
|
||||
{"fieldName": "niveau", "operator": "=", "value": 0},
|
||||
{
|
||||
"fieldName": "usableOnImmobilisation",
|
||||
"operator": "=",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<FamilleProduit> familleProduits = [];
|
||||
for (var familleProduit in response.data["data"]) {
|
||||
familleProduits.add(FamilleProduit.fromJson(familleProduit));
|
||||
}
|
||||
return familleProduits;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<FamilleProduit>?> getSousFamilleProduit(parentId) async {
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search',
|
||||
data: {
|
||||
"data": {
|
||||
"_domain": "self.parente.id = :parente",
|
||||
"_domainContext": {"parente": parentId},
|
||||
"_archived": false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<FamilleProduit> sousfamilleProduits = [];
|
||||
for (var sousfamilleProduit in response.data["data"]) {
|
||||
sousfamilleProduits.add(FamilleProduit.fromJson(sousfamilleProduit));
|
||||
}
|
||||
return sousfamilleProduits;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<InventoryLine>?> getMyInventoryLines() async {
|
||||
final user = await fetchUserProfile();
|
||||
|
||||
final response = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search',
|
||||
data: {
|
||||
"data": {
|
||||
"_domain":
|
||||
"self.createdBy.id = :createdBy and self.inventory.id = :inventoryId",
|
||||
"_domainContext": {
|
||||
"inventoryId": kInventoryId,
|
||||
"createdBy": user!['id'],
|
||||
},
|
||||
"_archived": false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||
List<InventoryLine>? inventoryLines = [];
|
||||
for (var inventoryLine in response.data["data"]) {
|
||||
inventoryLines.add(InventoryLine.fromJson(inventoryLine));
|
||||
}
|
||||
return inventoryLines;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<TrackingNumber?> createTrackingNumber({
|
||||
required TrackingNumber trackingNumber,
|
||||
}) async {
|
||||
final String url =
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber';
|
||||
|
||||
try {
|
||||
// 1. Check if product already exists
|
||||
final searchResponse = await dio.post(
|
||||
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search',
|
||||
data: {
|
||||
"data": {
|
||||
"_domain":
|
||||
"self.trackingNumberSeq = :trackingNumberSeq and self.product.id = :productId",
|
||||
"_domainContext": {
|
||||
"trackingNumberSeq": trackingNumber.trackingNumberSeq,
|
||||
"productId": trackingNumber.product,
|
||||
},
|
||||
"_archived": false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (searchResponse.statusCode == 200 &&
|
||||
searchResponse.data['status'] == 0) {
|
||||
final existing = searchResponse.data['data'];
|
||||
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
Get.snackbar('Erreur', "N° de serie existe deja");
|
||||
return null;
|
||||
} else {}
|
||||
}
|
||||
|
||||
// 2. Create or update the InventoryLine
|
||||
final saveResponse = await dio.post(
|
||||
url,
|
||||
data: jsonEncode({"data": trackingNumber.toJson()}),
|
||||
);
|
||||
|
||||
if (saveResponse.statusCode == 200) {
|
||||
final result = saveResponse.data;
|
||||
if (result['status'] == 0) {
|
||||
return TrackingNumber.fromJson(result["data"][0]);
|
||||
} else {}
|
||||
} else {}
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> createInitialInventoryLine(
|
||||
Product product,
|
||||
TrackingNumber? trackingNumber,
|
||||
Depot? depot,
|
||||
) async {
|
||||
final user = await fetchUserProfile();
|
||||
|
||||
final line = InventoryLine(
|
||||
inventoryId: kInventoryId,
|
||||
productId: product.id!,
|
||||
productName: product.name ?? '',
|
||||
currentQty: 1,
|
||||
realQty: 1,
|
||||
description: "Initial entry from mobile",
|
||||
unitId: product.unit?.id ?? 4,
|
||||
countingTypeSelect: 1,
|
||||
firstCounting: 1,
|
||||
firstCountingDate: DateTime.now().toIso8601String(),
|
||||
firstCountingByUser: user,
|
||||
stockLocationId: depot!.id!,
|
||||
ticketId: product.internalDescription ?? '',
|
||||
secondCounting: null,
|
||||
thirdCounting: null,
|
||||
trackingNumberId: trackingNumber?.id,
|
||||
);
|
||||
|
||||
await saveInventoryLine(inventoryLine: line);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user