Files
ERP-APP-INVENTORY/lib/service/axelor_client.dart
BACHIR SOULDI 5a483da01d 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.
2026-07-16 14:37:28 +01:00

641 lines
19 KiB
Dart

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