Compare commits

..

No commits in common. "master" and "0.6.1" have entirely different histories.

214 changed files with 9432 additions and 71254 deletions

3
.fvmrc
View file

@ -1,3 +0,0 @@
{
"flutter": "3.32.4"
}

32
.github/release.yml vendored
View file

@ -1,32 +0,0 @@
# .github/release.yml
changelog:
exclude:
labels:
- wontfix
- translation
categories:
- title: Breaking Changes
labels:
- Semver-Major
- breaking
- title: Security Patches
labels:
- security
- title: New Features
labels:
- Semver-Minor
- enhancement
- title: Bug Fixes
labels:
- Semver-Patch
- bug
- title: Devops / Setup Changes
labels:
- docker
- setup
- demo
- CI
- title: Other Changes
labels:
- "*"

View file

@ -3,10 +3,10 @@
name: Android name: Android
on: on:
pull_request: push:
branches: branches:
- master - master
push: pull_request:
branches: branches:
- master - master
@ -17,43 +17,23 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v3 uses: actions/setup-java@v1
with: with:
distribution: 'temurin' java-version: '12.x'
java-version: '17' - name: Setup Flutter
uses: subosito/flutter-action@v1
- name: Setup FVM
id: fvm-config-action
uses: kuhnroyal/flutter-fvm-config-action@v2
- uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }} flutter-version: '2.10.3'
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
cache: false
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
- run: flutter --version
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2 uses: gradle/gradle-build-action@v2
with: with:
gradle-version: 8.7 gradle-version: 6.1.1
- name: Collect Translation Files
run: |
cd lib/l10n
python3 collect_translations.py
- name: Build for Android - name: Build for Android
run: | run: |
dart pub global activate fvm flutter pub get
fvm install cp lib/dummy_dsn.dart lib/dsn.dart
fvm flutter pub get flutter build apk --debug
fvm flutter build apk --debug

View file

@ -1,93 +0,0 @@
name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INVENTREE_SITE_URL: http://localhost:8000
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
INVENTREE_MEDIA_ROOT: ../test_inventree_media
INVENTREE_STATIC_ROOT: ../test_inventree_static
INVENTREE_BACKUP_DIR: ../test_inventree_backup
INVENTREE_ADMIN_USER: testuser
INVENTREE_ADMIN_PASSWORD: testpassword
INVENTREE_ADMIN_EMAIL: test@test.com
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Flutter and FVM
id: fvm-config-action
uses: kuhnroyal/flutter-fvm-config-action@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
- name: Collect Translation Files
run: |
cd lib/l10n
python collect_translations.py
- name: Static Analysis Tests
working-directory: .
run: |
python ./find_dart_files.py
dart pub global activate fvm
fvm install
fvm flutter pub get
fvm flutter analyze
dart format --output=none --set-exit-if-changed .
- name: Start InvenTree Server
run: |
sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++
pip3 install invoke
git clone --depth 1 https://github.com/inventree/inventree ./inventree_server
cd inventree_server
invoke install
invoke migrate
invoke dev.import-fixtures
invoke dev.server -a 127.0.0.1:8000 &
invoke wait
sleep 30
- name: Unit Tests
run: |
fvm flutter test --coverage
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -3,10 +3,10 @@
name: iOS name: iOS
on: on:
pull_request: push:
branches: branches:
- master - master
push: pull_request:
branches: branches:
- master - master
@ -17,44 +17,23 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v3 uses: actions/setup-java@v1
with: with:
distribution: 'temurin' java-version: '12.x'
java-version: '11'
- name: Setup FVM
id: fvm-config-action
uses: kuhnroyal/flutter-fvm-config-action@v2
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v1
with: with:
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }} flutter-version: '2.10.3'
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
cache: false
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
- name: Collect Translation Files
run: |
cd lib/l10n
python3 collect_translations.py
- name: Build for iOS - name: Build for iOS
run: | run: |
dart pub global activate fvm flutter pub get
fvm install
fvm flutter pub get
fvm flutter precache --ios
cd ios cd ios
pod repo update pod repo update
pod install pod install
cd .. cd ..
fvm flutter build ios --release --no-codesign --no-tree-shake-icons cp lib/dummy_dsn.dart lib/dsn.dart
flutter build ios --release --no-codesign

37
.github/workflows/lint.yaml vendored Normal file
View file

@ -0,0 +1,37 @@
# Run flutter linting checks
name: lint
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
runs-on: ubuntu-latest
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
flutter-version: '2.10.3'
- run: flutter pub get
- run: cp lib/dummy_dsn.dart lib/dsn.dart
- run: flutter analyze
- run: flutter test --coverage

8
.gitignore vendored
View file

@ -11,9 +11,8 @@
coverage/* coverage/*
# This file is auto-generated as part of the CI process # Sentry API key
test/coverage_helper_test.dart lib/dsn.dart
InvenTreeSettings.db
# App signing key # App signing key
android/key.properties android/key.properties
@ -82,6 +81,3 @@ ios/Podfile.lock
!**/ios/**/default.pbxuser !**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3 !**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# FVM Version Cache
.fvm/

3
.gitmodules vendored
View file

@ -0,0 +1,3 @@
[submodule "lib/l10n"]
path = lib/l10n
url = git@github.com:inventree/inventree-app-i18n.git

View file

@ -1,8 +0,0 @@
repos:
- repo: local
hooks:
- id: dart-format
name: Dart Format
entry: dart format
language: system
types: [dart]

View file

@ -1,132 +0,0 @@
## InvenTree App Development
For developers looking to contribute to the project, we use Flutter for app development. The project has been tested in Android Studio (on both Windows and Mac) and also VSCode.
## Prerequisites
To build the app from source, you will need the following tools installed on your system:
- Android Studio or Visual Studio Code (with Flutter and Dart plugins)
- [Flutter Version Management (FVM)](https://fvm.app/) - We use FVM to manage Flutter versions
### iOS Development
For iOS development, you will need a Mac system with XCode installed.
### Java Version
Some versions of Android Studio ship with a built-in version of the Java JDK. However, the InvenTree app requires [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to be installed.
If you see any errors related to JDK version mismatch, download and install the correct version of the JDK (from the link above) and update your Android Studio settings to point to the correct JDK location:
```bash
fvm flutter config --jdk-dir /path/to/jdk
```
## Invoke Tasks
We use the [invoke](https://www.pyinvoke.org) to run some core tasks - you will need python and invoke installed on your local system.
## Flutter Version Management (FVM)
This project uses [Flutter Version Management (FVM)](https://fvm.app/) to ensure consistent Flutter versions across development environments and CI/CD pipelines.
For installation instructions, please refer to the [official FVM documentation](https://fvm.app/documentation/getting-started/installation).
Once installed, FVM will automatically use the Flutter version specified in the `.fvmrc` file at the root of the project.
### Visual Studio Code
To set up Visual Studio Code, you will need to make sure the `.vscode` directory exists. Then run `fvm use` to ensure the correct Flutter version is used.
```
mkdir -p .vscode
fvm use
```
#### What happens:
- Downloads SDK if not cached
- Creates `.fvm` directory with SDK symlink
- Updates `.fvmrc` configuration
- Configures IDE settings
- Runs `flutter pub get`
### Android Studio
To set up Android Studio, run `fvm use` to ensure the correct Flutter version is used.
```
fvm use
```
#### What happens:
- Downloads SDK if not cached
- Creates `.fvm` directory with SDK symlink
- Updates `.fvmrc` configuration
- Runs `flutter pub get`
Set Flutter SDK path in Android Studio:
1. Open Android Studio
2. Go to `File` -> `Settings` -> `Languages & Frameworks` -> `Flutter`
3. Set `Flutter SDK path` to `.fvm/flutter_sdk`:
![Setting Flutter SDK path in Android Studio](docs/android_studio_fvm.png)
## Getting Started
Initial project setup (after you have installed all required dev tools) is as follows:
Generate initial translation files:
```
invoke translate
```
Install required flutter packages:
```
fvm flutter pub get
```
You should now be ready to debug on a connected or emulated device!
## Troubleshooting
### Flutter Doctor
If you're experiencing issues with the development environment, run Flutter Doctor to diagnose problems:
```bash
fvm flutter doctor -v
```
This will check your Flutter installation and identify any issues with your setup. Common issues include:
- Missing Android SDK components
- iOS development tools not properly configured
- Missing dependencies
Fix any identified issues before proceeding with development.
## Building Release Versions
Building release versions for target platforms (either android or iOS) is simplified using invoke:
### Android
Build Android release:
```
invoke android
```
### iOS
Build iOS release:
```
invoke ios
```

View file

@ -1,50 +0,0 @@
# Contributing to InvenTree App
Thank you for considering contributing to the InvenTree App! This document outlines some guidelines to ensure smooth collaboration.
## Code Style and Formatting
### Dart Formatting
We enforce consistent code formatting using Dart's built-in formatter. Before submitting a pull request:
1. Run the formatter on your code:
```bash
fvm dart format .
```
2. Our CI pipeline will verify that all code follows the standard Flutter/Dart formatting rules. Pull requests with improper formatting will fail CI checks.
### General Guidelines
- Write clear, readable, and maintainable code
- Include comments where necessary
- Follow Flutter/Dart best practices
- Write tests for new features when applicable
## Pull Request Process
1. Fork the repository and create a feature branch
2. Make your changes
3. Ensure your code passes all tests and linting
4. Format your code using `invoke format`
5. Submit a pull request with a clear description of the changes
6. Address any review comments
## Development Setup
1. Ensure you have Flutter installed (we use Flutter Version Management)
2. Check the required Flutter version in the `.fvmrc` file
3. Install dependencies with `fvm flutter pub get`
4. Run tests with `fvm flutter test`
## Reporting Issues
When reporting issues, please include:
- Clear steps to reproduce the issue
- Expected behavior
- Actual behavior
- Screenshots if applicable
- Device/environment information
Thank you for contributing to the InvenTree App!

View file

@ -3,36 +3,11 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Android](https://github.com/inventree/inventree-app/actions/workflows/android.yaml/badge.svg) ![Android](https://github.com/inventree/inventree-app/actions/workflows/android.yaml/badge.svg)
![iOS](https://github.com/inventree/inventree-app/actions/workflows/ios.yaml/badge.svg) ![iOS](https://github.com/inventree/inventree-app/actions/workflows/ios.yaml/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/inventree/inventree-app/badge.svg?branch=master)](https://coveralls.io/github/inventree/inventree-app?branch=master)
The InvenTree mobile / tablet application is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree). The InvenTree mobile / tablet application is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree).
Written in the [Flutter](https://flutter.dev/) environment, the app provides native support for Android and iOS devices. Written in the [Flutter](https://flutter.dev/) environment, the app provides native support for Android and iOS devices.
<p align="center">
<img width="30%" src="https://github.com/user-attachments/assets/aee96f90-2953-47f6-916a-06f19d3b8aa5">
</p>
## Installation
You can install the app via the following channels:
### Google Play Store (Android)
Download and install from the [Google Play Store](https://play.google.com/store/apps/details?id=inventree.inventree_app&hl=en_AU)
### Apple Store (iOS)
Download and install from the [Apple App Store](https://apps.apple.com/au/app/inventree/id1581731101)
### Direct Download (Android)
We provide direct downloads for Android users - view our [download page via polar.sh](https://polar.sh/inventree/products/299bf0d5-af88-4e0f-becf-c007ad37ecf2)
## User Documentation ## User Documentation
User documentation for the InvenTree mobile app can be found [within the InvenTree documentation](https://inventree.readthedocs.io/en/latest/app/app/). User documentation for the InvenTree mobile app can be found [within the InvenTree documentation](https://inventree.readthedocs.io/en/latest/app/app/).
## Developer Documentation
Refer to the [build instructions](BUILDING.md) for information on how to build the app from source.

47
RELEASE.md Normal file
View file

@ -0,0 +1,47 @@
# Release Process
## Android Play Store
[Reference](https://flutter.dev/docs/deployment/android#signing-the-app)
### Key File
Add a file `key.properties` under the android/ directory
### Increment Build Number
Make sure that the build number is incremented every time (or it will be rejected by Play Store).
### Copy Translations
Ensure that the translation files have been updated, and copied into the correct directory!!
```
cd lib/l10n
python update_translations.py
```
### Build Appbundle
`flutter build appbundle`
### Upload Appbundle
Upload the appbundle file to the Android developer website.
## Apple Store
Ref: https://flutter.dev/docs/deployment/ios
### Build ipa
```
flutter clean
flutter build ipa
```
### Validate and Distribute
- Open `./build/ios/archive/Runner.xcarchive` in Xcode
- Run "Validate App"
- Run "Distribute App"

View file

@ -6,6 +6,8 @@ analyzer:
- lib/generated/** - lib/generated/**
language: language:
strict-raw-types: true strict-raw-types: true
strong-mode:
implicit-casts: false
linter: linter:
rules: rules:
@ -19,8 +21,6 @@ linter:
prefer_double_quotes: true prefer_double_quotes: true
unreachable_from_main: false
prefer_final_locals: false prefer_final_locals: false
prefer_const_constructors: false prefer_const_constructors: false
@ -74,12 +74,4 @@ linter:
avoid_dynamic_calls: false avoid_dynamic_calls: false
avoid_classes_with_only_static_members: false avoid_classes_with_only_static_members: false
no_leading_underscores_for_local_identifiers: false
use_super_parameters: false
# TODO: Enable unnecessary_async and unawaited_futures rules
unnecessary_async: false

14
android/.gitignore vendored
View file

@ -1,14 +0,0 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View file

@ -1,9 +1,3 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -12,6 +6,11 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@ -22,6 +21,10 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@ -29,18 +32,7 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
namespace "inventree.inventree_app" compileSdkVersion 31
compileSdkVersion 35
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
// If using Kotlin
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
@ -56,8 +48,8 @@ android {
defaultConfig { defaultConfig {
applicationId "inventree.inventree_app" applicationId "inventree.inventree_app"
minSdkVersion 21 minSdkVersion 25
targetSdkVersion 35 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -91,6 +83,7 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support:multidex:2.0.1' androidTestImplementation 'com.android.support:multidex:2.0.1'
implementation "androidx.core:core:1.9.0" implementation "androidx.core:core:1.5.0-rc01"
implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
} }

View file

@ -1,6 +1,4 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="inventree.inventree_app"> package="inventree.inventree_app">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
@ -31,6 +29,10 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- until Flutter renders its first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<!-- Theme to apply as soon as Flutter begins rendering frames --> <!-- Theme to apply as soon as Flutter begins rendering frames -->
<meta-data <meta-data
@ -51,12 +53,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.MICROPHONE"/> <uses-permission android:name="android.permission.MICROPHONE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--
Prevent lower level dependencies from including specific permissions.
Ref: https://developer.android.com/studio/build/manage-manifests
-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
<uses-permission android:name="android.permission.INSTALL_PACKAGES" tools:node="remove"/>
</manifest> </manifest>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowSplashScreenBackground">@color/splash_screen_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/launch_background</item>
</style>
</resources>

View file

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="inventree.inventree_app">
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->

View file

@ -1,8 +1,22 @@
buildscript {
ext.kotlin_version = '1.5.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
mavenCentral() jcenter()
} }
} }
@ -15,6 +29,6 @@ subprojects {
} }
} }
tasks.register("clean", Delete) { task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -1,8 +1,4 @@
org.gradle.daemon=true org.gradle.jvmargs=-Xmx1536M
org.gradle.parallel=true android.enableR8=true
org.gradle.configureondemand=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096M
android.enableD8=true
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true

View file

@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=30000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

View file

@ -1,25 +1,15 @@
pluginManagement { include ':app'
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
repositories { def plugins = new Properties()
google() def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
mavenCentral() if (pluginsFile.exists()) {
gradlePluginPortal() pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
} }
plugins { plugins.each { name, path ->
id "dev.flutter.flutter-plugin-loader" version "1.0.0" def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
id "com.android.application" version "8.6.0" apply false include ":$name"
id "org.jetbrains.kotlin.android" version "1.9.25" apply false project(":$name").projectDir = pluginDirectory
} }
include ":app"

View file

@ -1,23 +1,6 @@
## Contributors ## InvenTree App Credits
---
Thanks to the following contributors, for their work building this app! ### Sound Files
- [SchrodingersGat](https://github.com/SchrodingersGat) (*Lead Developer*) - Some sound files have been sourced from [https://www.zapsplat.com](https://www.zapsplat.com)
- [cbenhagen](https://github.com/cbenhagen)
- [Guusggg](https://github.com/Guusggg)
- [GoryMoon](https://github.com/GoryMoon)
- [simonkuehling](https://github.com/simonkuehling)
- [Bobbe](https://github.com/30350n)
- [awnz](https://github.com/awnz)
- [joaomnuno](https://github.com/joaomnuno)
- [Alex9779](https://github.com/Alex9779)
--------
## Assets
The InvenTree App makes use of the following third party assets
- Icons are provided by [tabler.io](https://tabler.io/icons)
- Sound files have been sourced from [zapsplat](https://www.zapsplat.com)
--------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

View file

@ -1,468 +1,6 @@
### x.xx.x - Month Year ## InvenTree App Release Notes
--- ---
- Support display of custom status codes
- Fix default values for list sorting
### 0.21.2 - January 2026
---
- Fixes bug which launched camera twice when uploading an attachment
- Fixed bug related to list sorting and filtering
### 0.21.1 - November 2025
---
- Fixed app freeze bug after form submission
### 0.21.0 - November 2025
---
- Support label printing again, fixing issues with new printing API
- Adds zoom controller for barcode scanner camera view
- Display default stock location in Part detail page
- Display stock information in SupplierPart detail page
### 0.20.2 - November 2025
---
- Fixes URL for reporting issues on GitHub
- Fix for uploading files against server with self-signed certificates
### 0.20.1 - October 2025
---
- Bug fix for camera barcode scanner
### 0.20.0 - October 2025
---
- View pending shipments from the home screen
- Display detail view for shipments
- Adds ability to ship pending outgoing shipments
- Adds ability to mark outgoing shipments as "checked" or "unchecked"
- Updated translations
### 0.19.3 - September 2025
---
- Fixes incorrect priority of barcode scanner results
### 0.19.2 - August 2025
---
- Allow purchase orders to be completed
- Improved UX across the entire app
- Fix bug which prevented display of part images for purchase order line items
### 0.19.1 - July 2025
---
- Fixes bug related to barcode scanning with certain devices
### 0.19.0 - June 2025
---
- Replace barcode scanning library for better performance
- Display part pricing information
- Updated theme support
- Fix broken documentation link
- Reduce frequency of notification checks
- Updated translations
- Add image cropping functionality
### 0.18.1 - April 2025
---
- Fix bug associated with handling invalid URLs
### 0.18.0 - April 2025
---
- Adds ability to create new companies from the app
- Allow creation of line items against pending sales orders
- Support "extra line items" for purchase orders
- Support "extra line items" for sales orders
- Display start date for purchase orders
- Display start date for sales orders
- Fix scrolling behaviour for some widgets
- Updated search functionality
- Updated translations
### 0.17.4 - January 2025
---
- Display responsible owner for orders
- Display completion date for orders
- Updated translations
### 0.17.3 - January 2025
---
- Fixes bug which prevent dialog boxes from being dismissed correctly
- Enable editing of attachment comments
- Updated translations
### 0.17.2 - December 2024
---
- Fixed error message when printing a label to a remote machine
- Prevent notification sounds from pause media playback
- Display stock expiry information
- Updated translations
### 0.17.1 - December 2024
---
- Add support for ManufacturerPart model
- Support barcode scanning for ManufacturerPart
- Fix bugs in global search view
- Fixes barcode scanning bug which prevents scanning of DataMatrix codes
- Display "destination" information in PurchaseOrder detail view
- Pre-fill "location" field when receiving items against PurchaseOrder
- Fix display of part name in PurchaseOrderLineItem list
- Adds "assigned to me" filter for Purchase Order list
- Adds "assigned to me" filter for Sales Order list
- Updated translations
### 0.17.0 - December 2024
---
- Improved barcode scanning with new scanning library
- Prevent screen turning off when scanning barcodes
- Improved support for Stock Item test results
- Enhanced home-screen display using grid-view
- Improvements for image uploading
- Provide "upload image" shortcut on Purchase Order detail view
- Provide "upload image" shortcut on Sales Order detail view
- Clearly indicate if a StockItem is unavailable
- Improved list filtering management
- Updated translations
### 0.16.5 - September 2024
---
- Allow blank values to be entered into numerical fields
- Updated translations
### 0.16.4 - September 2024
---
- Fixes bug related to printing stock item labels
### 0.16.3 - August 2024
---
- Fixes bug relating to viewing attachment files
- Fixes bug relating to uploading attachment files
### 0.16.2 - August 2024
---
- Support "ON_HOLD" status for Purchase Orders
- Support "ON_HOLD" status for Sales Orders
- Change base icon package from FontAwesome to TablerIcons
- Bug fixes for barcode scanning
- Translation updates
### 0.16.1 - July 2024
---
- Update base packages for Android
### 0.16.0 - June 2024
---
- Add support for new file attachments API
- Drop support for legacy servers with API version < 100
### 0.15.0 - June 2024
---
- Support modern label printing API
- Improved display of stock item serial numbers
- Updated translations
### 0.14.3 - April 2024
---
- Support "active" field for Company model
- Support "active" field for SupplierPart model
- Adjustments to barcode scanning workflow
- Updated translations
### 0.14.2 - February 2024
---
- Updated error reporting
- Support for updated server API endpoints
- Updated translations
### 0.14.1 - January 2024
---
- Squashing bugs
### 0.14.0 - December 2023
---
- Adds support for Sales Orders
- Adds option to pause and resume barcode scanning with camera
- Adds option for "single shot" barcode scanning with camera
- Fixes bug when removing entire quantity of a stock item
- Add line items to purchase orders directly from the app
- Add line items to purchase order using barcode scanner
- Add line items to sales orders directly from the app
- Add line items to sales order using barcode scanner
- Allocate stock items against existing sales orders
### 0.13.0 - October 2023
---
- Adds "wedge scanner" mode, allowing use with external barcode readers
- Add ability to scan in received items using supplier barcodes
- Store API token, rather than username:password
- Ensure that user will lose access if token is revoked by server
- Improve scroll-to-refresh behaviour across multiple widgets
### 0.12.8 - September 2023
---
- Added extra options for transferring stock items
- Fixes bug where API data was not fetched with correct locale
### 0.12.7 - August 2023
---
- Bug fix for Supplier Part editing page
- Bug fix for label printing (blank template names)
- Updated translations
### 0.12.6 - July 2023
---
- Enable label printing for stock locations
- Enable label printing for parts
- Updated translation support
- Bug fixes
### 0.12.5 - July 2023
---
- Adds extra filtering options for stock items
- Updated translations
### 0.12.4 - July 2023
---
- Pre-fill stock location when transferring stock amount
- UX improvements for searching data
- Updated translations
### - 0.12.3 - June 2023
---
- Edit part parameters from within the app
- Increase visibility of stock quantity in widgets
- Improved filters for stock list
- Bug fix for editing stock item purchase price
### 0.12.2 - June 2023
---
- Adds options for configuring screen orientation
- Improvements to barcode scanning
- Translation updates
- Bug fix for scrolling long lists
### 0.12.1 - May 2023
---
- Fixes bug in purchase order form
### 0.12.0 - April 2023
---
- Add support for Project Codes
- Improve purchase order support
- Fix action button colors
- Improvements for stock item test result display
- Added Norwegian translations
- Fix serial number field when creating stock item
### 0.11.5 - April 2023
---
- Fix background image transparency for dark mode
- Fix link to Bill of Materials from Part screen
- Improvements to supplier part detail screen
- Add "notes" field to more models
### 0.11.4 - April 2023
---
- Bug fix for stock history widget
- Improved display of stock history widget
- Theme improvements for dark mode
### 0.11.3 - April 2023
---
- Fixes text color in dark mode
### 0.11.2 - April 2023
---
- Adds "dark mode" display option
- Add action to issue a purchase order
- Add action to cancel a purchase order
- Reimplement periodic checks for notifications
### 0.11.1 - April 2023
---
- Fixes keyboard bug in search widget
- Adds ability to create new purchase orders directly from the app
- Adds support for the "contact" field to purchase orders
- Improved rendering of status codes for stock items
- Added rendering of status codes for purchase orders
### 0.11.0 - April 2023
---
- Major UI updates - [see the documentation](https://docs.inventree.org/en/latest/app/app/)
- Adds globally accessible action button for "search"
- Adds globally accessible action button for "barcode scan"
- Implement context actions using floating actions buttons
- Support barcode scanning for purchase orders
### 0.10.2 - March 2023
---
- Adds support for proper currency rendering
- Fix icon for supplier part detail widget
- Support global search API endpoint
- Updated translations
### 0.10.1 - February 2023
---
- Add support for attachments on Companies
- Fix duplicate scanning of barcodes
- Updated translations
### 0.10.0 - February 2023
---
- Add support for Supplier Parts
- Updated translations
### 0.9.3 - February 2023
---
- Updates to match latest server API
- Bug fix for empty HttpResponse from server
### 0.9.2 - December 2022
---
- Support custom icons for part category
- Support custom icons for stock location
- Adjustments to notification messages
- Assorted bug fixes
- Updated translations
### 0.9.1 - December 2022
---
- Bug fixes for custom barcode actions
- Updated translations
### 0.9.0 - December 2022
---
- Added support for custom barcodes for Parts
- Added support for custom barcode for Stock Locations
- Support Part parameters
- Add support for structural part categories
- Add support for structural stock locations
- Allow deletion of attachments via app
- Adds option for controlling BOM display
- Updated translations
### 0.8.3 - September 2022
---
- Display list of assemblies which components are used in
- Fixes search input bug
### 0.8.2 - August 2022
---
- Allow serial numbers to be specified when creating new stock items
- Allow serial numbers to be edited for existing stock items
- Allow app locale to be changed manually
- Improved handling of certain errors
### 0.8.1 - August 2022
---
- Added extra filtering options for PartCategory list
- Added extra filtering options for StockLocation list
- Fixed bug related to null widget context
- Improved error handling and reporting
### 0.8.0 - July 2022
---
- Display part variants in the part detail view
- Display Bill of Materials in the part detail view
- Indicate available quantity in stock detail view
- Adds configurable filtering to various list views
- Allow stock location to be "scanned" into another location using barcode
- Improves server connection status indicator on home screen
- Display loading indicator during long-running operations
- Improved error handling and reporting
### 0.7.3 - June 2022
---
- Adds ability to display link URLs in attachments view
- Updated translations
### 0.7.2 - June 2022
---
- Add "quarantined" status flag for stock items
- Extends attachment support to stock items
- Extends attachment support to purchase orders
### 0.7.1 - May 2022
---
- Fixes issue which prevented text input in search window
- Remove support for legacy stock adjustment API
- App now requires server API version 20 (or newer)
- Updated translation files
### 0.7.0 - May 2022
---
- Refactor home screen display
- Display notification messages from InvenTree server
- Fixes duplicated display of units when showing stock quantity
- Adds ability to locate / identify stock items or locations (requires server plugin)
- Improve rendering of home screen when server is not connected
- Adds ability to load global and user settings from the server
- Translation updates
### 0.6.2 - April 2022
---
- Fixes issues related to locale support (for specific locales)
### 0.6.1 - April 2022 ### 0.6.1 - April 2022
--- ---

View file

@ -1,3 +0,0 @@
files:
- source: /lib/l10n/app_en.arb
translation: /lib/l10n/%locale_with_underscore%/app_%locale_with_underscore%.arb

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View file

@ -1,50 +0,0 @@
"""
This script recursively finds any '.dart' files in the ./lib directory,
and generates a 'test' file which includes all these files.
This is to ensure that *all* .dart files are included in test coverage.
By default, source files which are not touched by the unit tests are not included!
Ref: https://github.com/flutter/flutter/issues/27997
"""
from pathlib import Path
if __name__ == "__main__":
dart_files = Path("lib").rglob("*.dart")
print("Discovering dart files...");
with open("test/coverage_helper_test.dart", "w") as f:
f.write("// ignore_for_file: unused_import\n\n")
f.write("// dart format off\n\n")
skips = [
"generated",
"l10n",
"dsn.dart",
]
for path in dart_files:
path = str(path)
if any([s in path for s in skips]):
continue
# Remove leading 'lib\' text
path = path[4:]
path = path.replace("\\", "/")
f.write(f'import "package:inventree/{path}";\n')
f.write("\n\n")
f.write(
"// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n"
)
f.write(
"// It has been created to ensure that *all* source file are included in coverage data\n"
)
f.write('import "package:test/test.dart";\n\n')
f.write("// Do not actually test anything!\n")
f.write("void main() {}\n")

34
ios/.gitignore vendored
View file

@ -1,34 +0,0 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>12.0</string> <string>9.0</string>
</dict> </dict>
</plist> </plist>

View file

@ -1,32 +0,0 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View file

@ -1,5 +0,0 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View file

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your projects # Uncomment this line to define a global platform for your projects
platform :ios, '15.0' platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -39,7 +39,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
end end
end end
end end

View file

@ -3,13 +3,13 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 46;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@ -57,7 +57,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
D95D9CD46BE28F7F69DBC0F6 /* Pods_Runner.framework in Frameworks */, D95D9CD46BE28F7F69DBC0F6 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -94,7 +93,6 @@
3B8B22940C363C2F0DDB698A /* Frameworks */, 3B8B22940C363C2F0DDB698A /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
tabWidth = 5;
}; };
97C146EF1CF9000F007C117D /* Products */ = { 97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
@ -159,9 +157,6 @@
dependencies = ( dependencies = (
); );
name = Runner; name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -172,28 +167,26 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 0910;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "The Chromium Authors"; ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1; CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = A5RYN267BH;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
}; };
}; };
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2"; compatibilityVersion = "Xcode 3.2";
developmentRegion = en; developmentRegion = English;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
English,
en, en,
Base, Base,
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -210,6 +203,7 @@
files = ( files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
); );
@ -242,12 +236,10 @@
}; };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (
@ -258,7 +250,6 @@
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -280,45 +271,47 @@
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
"${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework", "${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework", "${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
"${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework", "${BUILT_PRODUCTS_DIR}/camera/camera.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/mobile_scanner/mobile_scanner.framework", "${BUILT_PRODUCTS_DIR}/open_file/open_file.framework",
"${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
"${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework", "${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
"${BUILT_PRODUCTS_DIR}/sqflite_darwin/sqflite_darwin.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
"${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mobile_scanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_filex.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@ -364,7 +357,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -374,17 +366,14 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -395,7 +384,6 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99; GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -404,8 +392,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -417,7 +404,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -429,12 +415,8 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@ -450,7 +432,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -460,17 +441,14 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -481,7 +459,6 @@
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99; GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -496,8 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -509,7 +485,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -519,17 +494,14 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -540,7 +512,6 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99; GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -549,8 +520,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -562,7 +532,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -574,12 +543,8 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@ -595,7 +560,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -607,12 +571,8 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = InvenTree; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@ -648,20 +608,6 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -1,28 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1510" LastUpgradeVersion = "0910"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@ -44,7 +26,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@ -62,13 +43,11 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -12,35 +12,23 @@
<string>6.0</string> <string>6.0</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>cs-CZ</string> <string>de</string>
<string>da-DK</string> <string>el</string>
<string>de-DE</string>
<string>el-GR</string>
<string>en</string> <string>en</string>
<string>es-ES</string> <string>es</string>
<string>es-MX</string> <string>fr</string>
<string>fa-IR</string> <string>he</string>
<string>fi-FI</string> <string>it</string>
<string>fr-FR</string> <string>ja</string>
<string>he-IL</string> <string>ko</string>
<string>hu-HU</string> <string>nl</string>
<string>id-ID</string> <string>no</string>
<string>it-IT</string> <string>pl</string>
<string>ja-JP</string> <string>ru</string>
<string>ko-KR</string> <string>sv</string>
<string>nl-NL</string> <string>tr</string>
<string>no-NO</string> <string>vi</string>
<string>pl-PL</string>
<string>pt-BR</string>
<string>pt-PT</string>
<string>ru-RU</string>
<string>sl_SI</string>
<string>sv-SE</string>
<string>th-TH</string>
<string>tr-TR</string>
<string>vi-VN</string>
<string>zh-CN</string> <string>zh-CN</string>
<string>zh-TW</string>
</array> </array>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>InvenTree</string> <string>InvenTree</string>
@ -79,16 +67,5 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>http</string>
<string>https</string>
<string>mailto</string>
<string>tel</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

View file

@ -1,4 +1,4 @@
arb-dir: lib/l10n/collected arb-dir: lib/l10n
template-arb-file: app_en.arb template-arb-file: app_en.arb
output-localization-file: app_localizations.dart output-localization-file: app_localizations.dart
output-class: I18N output-class: I18N

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,19 @@
import "package:adaptive_theme/adaptive_theme.dart";
import "package:flutter/material.dart";
import "package:inventree/helpers.dart";
import "package:one_context/one_context.dart";
bool isDarkMode() {
if (!hasContext()) {
return false;
}
BuildContext? context = OneContext().context; import "dart:ui";
if (context == null) { const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
return false; const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
}
return AdaptiveTheme.of(context).brightness == Brightness.dark; const Color COLOR_CLICK = Color.fromRGBO(150, 120, 100, 0.9);
}
// Return an "action" color based on the current theme const Color COLOR_BLUE = Color.fromRGBO(0, 0, 250, 1);
Color get COLOR_ACTION {
if (isDarkMode()) {
return Colors.lightBlueAccent;
} else {
return Colors.blue;
}
}
// Set to null to use the system default const Color COLOR_STAR = Color.fromRGBO(250, 250, 100, 1);
Color? COLOR_APP_BAR;
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1); const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1); const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1); const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1); const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
const Color COLOR_SELECTED = Color.fromRGBO(0, 0, 0, 0.05);

68
lib/app_settings.dart Normal file
View file

@ -0,0 +1,68 @@
/*
* Class for managing app-level configuration options
*/
import "package:sembast/sembast.dart";
import "package:inventree/preferences.dart";
// Settings key values
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
const String INV_HOME_SHOW_PO = "homeShowPo";
const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers";
const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers";
const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
const String INV_SOUNDS_BARCODE = "barcodeSounds";
const String INV_SOUNDS_SERVER = "serverSounds";
const String INV_PART_SUBCATEGORY = "partSubcategory";
const String INV_STOCK_SUBLOCATION = "stockSublocation";
const String INV_STOCK_SHOW_HISTORY = "stockShowHistory";
const String INV_REPORT_ERRORS = "reportErrors";
const String INV_STRICT_HTTPS = "strictHttps";
class InvenTreeSettingsManager {
factory InvenTreeSettingsManager() {
return _manager;
}
InvenTreeSettingsManager._internal();
final store = StoreRef("settings");
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
Future<dynamic> getValue(String key, dynamic backup) async {
final value = await store.record(key).get(await _db);
if (value == null) {
return backup;
}
return value;
}
// Load a boolean setting
Future<bool> getBool(String key, bool backup) async {
final dynamic value = await getValue(key, backup);
if (value is bool) {
return value;
} else {
return backup;
}
}
Future<void> setValue(String key, dynamic value) async {
await store.record(key).put(await _db, value);
}
// Ensure we only ever create a single instance of this class
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
}

579
lib/barcode.dart Normal file
View file

@ -0,0 +1,579 @@
import "dart:io";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.dart";
import "package:qr_code_scanner/qr_code_scanner.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
import "package:inventree/api.dart";
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/stock_detail.dart";
class BarcodeHandler {
/*
* Class which "handles" a barcode, by communicating with the InvenTree server,
* and handling match / unknown / error cases.
*
* Override functionality of this class to perform custom actions,
* based on the response returned from the InvenTree server
*/
BarcodeHandler();
String getOverlayText(BuildContext context) => "Barcode Overlay";
QRViewController? _controller;
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Called when the server "matches" a barcode
// Override this function
}
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
// Called when the server does not know about a barcode
// Override this function
failureTone();
showSnackIcon(
L10().barcodeNoMatch,
success: false,
icon: Icons.qr_code,
);
}
Future<void> onBarcodeUnhandled(BuildContext context, Map<String, dynamic> data) async {
failureTone();
// Called when the server returns an unhandled response
showServerError(L10().responseUnknown, data.toString());
_controller?.resumeCamera();
}
Future<void> processBarcode(BuildContext context, QRViewController? _controller, String barcode, {String url = "barcode/"}) async {
this._controller = _controller;
print("Scanned barcode data: ${barcode}");
if (barcode.isEmpty) {
return;
}
var response = await InvenTreeAPI().post(
url,
body: {
"barcode": barcode,
},
expectedStatusCode: 200
);
_controller?.resumeCamera();
Map<String, dynamic> data = response.asMap();
// Handle strange response from the server
if (!response.isValid() || !response.isMap()) {
onBarcodeUnknown(context, {});
// We want to know about this one!
await sentryReportMessage(
"BarcodeHandler.processBarcode returned strange value",
context: {
"data": response.data?.toString() ?? "null",
"barcode": barcode,
"url": url,
"statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(),
"error": response.error,
"errorDetail": response.errorDetail,
}
);
} else if (data.containsKey("error")) {
onBarcodeUnknown(context, data);
} else if (data.containsKey("success")) {
onBarcodeMatched(context, data);
} else {
onBarcodeUnhandled(context, data);
}
}
}
class BarcodeScanHandler extends BarcodeHandler {
/*
* Class for general barcode scanning.
* Scan *any* barcode without context, and then redirect app to correct view
*/
@override
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
@override
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
failureTone();
showSnackIcon(
L10().barcodeNoMatch,
icon: FontAwesomeIcons.exclamationCircle,
success: false,
);
}
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
int pk = -1;
// A stocklocation has been passed?
if (data.containsKey("stocklocation")) {
pk = (data["stocklocation"]?["pk"] ?? -1) as int;
if (pk > 0) {
successTone();
InvenTreeStockLocation().get(pk).then((var loc) {
if (loc is InvenTreeStockLocation) {
Navigator.of(context).pop();
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
}
});
} else {
failureTone();
showSnackIcon(
L10().invalidStockLocation,
success: false
);
}
} else if (data.containsKey("stockitem")) {
pk = (data["stockitem"]?["pk"] ?? -1) as int;
if (pk > 0) {
successTone();
InvenTreeStockItem().get(pk).then((var item) {
// Dispose of the barcode scanner
Navigator.of(context).pop();
if (item is InvenTreeStockItem) {
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}
});
} else {
failureTone();
showSnackIcon(
L10().invalidStockItem,
success: false
);
}
} else if (data.containsKey("part")) {
pk = (data["part"]?["pk"] ?? -1) as int;
if (pk > 0) {
successTone();
InvenTreePart().get(pk).then((var part) {
// Dismiss the barcode scanner
Navigator.of(context).pop();
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
});
} else {
failureTone();
showSnackIcon(
L10().invalidPart,
success: false
);
}
} else {
failureTone();
showSnackIcon(
L10().barcodeUnknown,
success: false,
onAction: () {
OneContext().showDialog(
builder: (BuildContext context) => SimpleDialog(
title: Text(L10().unknownResponse),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(data.toString()),
)
],
)
);
}
);
}
}
}
class StockItemScanIntoLocationHandler extends BarcodeHandler {
/*
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
*/
StockItemScanIntoLocationHandler(this.item);
final InvenTreeStockItem item;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// If the barcode points to a "stocklocation", great!
if (data.containsKey("stocklocation")) {
// Extract location information
int location = (data["stocklocation"]["pk"] ?? -1) as int;
if (location == -1) {
showSnackIcon(
L10().invalidStockLocation,
success: false,
);
return;
}
// Transfer stock to specified location
final result = await item.transferStock(context, location);
if (result) {
successTone();
Navigator.of(context).pop();
showSnackIcon(
L10().barcodeScanIntoLocationSuccess,
success: true,
);
} else {
failureTone();
showSnackIcon(
L10().barcodeScanIntoLocationFailure,
success: false
);
}
} else {
failureTone();
showSnackIcon(
L10().invalidStockLocation,
success: false,
);
}
}
}
class StockLocationScanInItemsHandler extends BarcodeHandler {
/*
* Barcode handler for scanning stock item(s) into the specified StockLocation
*/
StockLocationScanInItemsHandler(this.location);
final InvenTreeStockLocation location;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Returned barcode must match a stock item
if (data.containsKey("stockitem")) {
int item_id = data["stockitem"]["pk"] as int;
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
if (item == null) {
failureTone();
showSnackIcon(
L10().invalidStockItem,
success: false,
);
} else if (item.locationId == location.pk) {
failureTone();
showSnackIcon(
L10().itemInLocation,
success: true
);
} else {
final result = await item.transferStock(context, location.pk);
if (result) {
successTone();
showSnackIcon(
L10().barcodeScanIntoLocationSuccess,
success: true
);
} else {
failureTone();
showSnackIcon(
L10().barcodeScanIntoLocationFailure,
success: false
);
}
}
} else {
failureTone();
// Does not match a valid stock item!
showSnackIcon(
L10().invalidStockItem,
success: false,
);
}
}
}
class UniqueBarcodeHandler extends BarcodeHandler {
/*
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
*/
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
// Callback function when a "unique" barcode hash is found
final Function(String) callback;
final String overlayText;
@override
String getOverlayText(BuildContext context) {
if (overlayText.isEmpty) {
return L10().barcodeScanAssign;
} else {
return overlayText;
}
}
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
failureTone();
// If the barcode is known, we can"t assign it to the stock item!
showSnackIcon(
L10().barcodeInUse,
icon: Icons.qr_code,
success: false
);
}
@override
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
// If the barcode is unknown, we *can* assign it to the stock item!
if (!data.containsKey("hash")) {
showServerError(
L10().missingData,
L10().barcodeMissingHash,
);
} else {
String hash = (data["hash"] ?? "") as String;
if (hash.isEmpty) {
failureTone();
showSnackIcon(
L10().barcodeError,
success: false,
);
} else {
successTone();
// Close the barcode scanner
Navigator.of(context).pop();
callback(hash);
}
}
}
}
class InvenTreeQRView extends StatefulWidget {
const InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
final BarcodeHandler _handler;
@override
State<StatefulWidget> createState() => _QRViewState(_handler);
}
class _QRViewState extends State<InvenTreeQRView> {
_QRViewState(this._handler) : super();
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
QRViewController? _controller;
final BarcodeHandler _handler;
bool flash_status = false;
Future<void> updateFlashStatus() async {
final bool? status = await _controller?.getFlashStatus();
flash_status = status != null && status;
// Reload
setState(() {
});
}
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
_controller!.pauseCamera();
}
_controller!.resumeCamera();
}
void _onViewCreated(BuildContext context, QRViewController controller) {
_controller = controller;
controller.scannedDataStream.listen((barcode) {
_controller?.pauseCamera();
if (barcode.code != null) {
_handler.processBarcode(context, _controller, barcode.code ?? "");
}
});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10().scanBarcode),
actions: [
IconButton(
icon: Icon(Icons.flip_camera_android),
onPressed: () {
_controller?.flipCamera();
}
),
IconButton(
icon: flash_status ? Icon(Icons.flash_off) : Icon(Icons.flash_on),
onPressed: () {
_controller?.toggleFlash();
updateFlashStatus();
},
)
],
),
body: Stack(
children: <Widget>[
Column(
children: [
Expanded(
child: QRView(
key: qrKey,
onQRViewCreated: (QRViewController controller) {
_onViewCreated(context, controller);
},
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: 300,
),
)
)
]
),
Center(
child: Column(
children: [
Spacer(),
Padding(
child: Text(_handler.getOverlayText(context),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white),
),
padding: EdgeInsets.all(20),
),
]
)
)
],
)
);
}
}
Future<void> scanQrCode(BuildContext context) async {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler())));
return;
}

View file

@ -1,439 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/helpers.dart";
import "package:inventree/inventree/sales_order.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/company/manufacturer_part_detail.dart";
import "package:inventree/widget/order/sales_order_detail.dart";
import "package:one_context/one_context.dart";
import "package:inventree/api.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/camera_controller.dart";
import "package:inventree/barcode/wedge_controller.dart";
import "package:inventree/barcode/controller.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/barcode/tones.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/part/part_detail.dart";
import "package:inventree/widget/order/purchase_order_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock/stock_detail.dart";
import "package:inventree/widget/company/company_detail.dart";
import "package:inventree/widget/company/supplier_part_detail.dart";
// Signal a barcode scan success to the user
Future<void> barcodeSuccess(String msg) async {
barcodeSuccessTone();
showSnackIcon(msg, success: true);
}
// Signal a barcode scan failure to the user
Future<void> barcodeFailure(String msg, dynamic extra) async {
barcodeFailureTone();
showSnackIcon(
msg,
success: false,
onAction: () {
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) => SimpleDialog(
title: Text(L10().barcodeError),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(extra.toString()),
),
],
),
);
}
},
);
}
/*
* Launch a barcode scanner with a particular context and handler.
*
* - Can be called with a custom BarcodeHandler instance, or use the default handler
* - Returns a Future which resolves when the scanner is dismissed
* - The provided BarcodeHandler instance is used to handle the scanned barcode
*/
Future<Object?> scanBarcode(
BuildContext context, {
BarcodeHandler? handler,
}) async {
// Default to generic scan handler
handler ??= BarcodeScanHandler();
InvenTreeBarcodeController controller = CameraBarcodeController(handler);
// Select barcode controller based on user preference
final int barcodeControllerType =
await InvenTreeSettingsManager().getValue(
INV_BARCODE_SCAN_TYPE,
BARCODE_CONTROLLER_CAMERA,
)
as int;
switch (barcodeControllerType) {
case BARCODE_CONTROLLER_WEDGE:
controller = WedgeBarcodeController(handler);
case BARCODE_CONTROLLER_CAMERA:
default:
// Already set as default option
break;
}
return Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (context, _, _) => controller, opaque: false),
);
}
/*
* Class for general barcode scanning.
* Scan *any* barcode without context, and then redirect app to correct view.
*
* Handles scanning of:
*
* - StockLocation
* - StockItem
* - Part
* - SupplierPart
* - PurchaseOrder
*/
class BarcodeScanHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
L10().barcodeNoMatch,
icon: TablerIcons.exclamation_circle,
success: false,
);
}
/*
* Response when a "Part" instance is scanned
*/
Future<void> handlePart(int pk) async {
var part = await InvenTreePart().get(pk);
if (part is InvenTreePart) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(builder: (context) => PartDetailWidget(part)),
);
}
}
/*
* Response when a "StockItem" instance is scanned
*/
Future<void> handleStockItem(int pk) async {
var item = await InvenTreeStockItem().get(pk);
if (item is InvenTreeStockItem) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(builder: (context) => StockDetailWidget(item)),
);
}
}
/*
* Response when a "StockLocation" instance is scanned
*/
Future<void> handleStockLocation(int pk) async {
var loc = await InvenTreeStockLocation().get(pk);
if (loc is InvenTreeStockLocation) {
OneContext().pop();
OneContext().navigator.push(
MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)),
);
}
}
/*
* Response when a "SupplierPart" instance is scanned
*/
Future<void> handleSupplierPart(int pk) async {
var supplierPart = await InvenTreeSupplierPart().get(pk);
if (supplierPart is InvenTreeSupplierPart) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(supplierPart),
),
);
}
}
/*
* Response when a "ManufacturerPart" instance is scanned
*/
Future<void> handleManufacturerPart(int pk) async {
var manufacturerPart = await InvenTreeManufacturerPart().get(pk);
if (manufacturerPart is InvenTreeManufacturerPart) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart),
),
);
}
}
Future<void> handleCompany(int pk) async {
var company = await InvenTreeCompany().get(pk);
if (company is InvenTreeCompany) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(builder: (context) => CompanyDetailWidget(company)),
);
}
}
/*
* Response when a "PurchaseOrder" instance is scanned
*/
Future<void> handlePurchaseOrder(int pk) async {
var order = await InvenTreePurchaseOrder().get(pk);
if (order is InvenTreePurchaseOrder) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order),
),
);
}
}
// Response when a SalesOrder instance is scanned
Future<void> handleSalesOrder(int pk) async {
var order = await InvenTreeSalesOrder().get(pk);
if (order is InvenTreeSalesOrder) {
OneContext().pop();
OneContext().push(
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(order)),
);
}
}
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
int pk = -1;
String model = "";
// The following model types can be matched with barcodes
List<String> validModels = [
InvenTreeStockItem.MODEL_TYPE,
InvenTreeSupplierPart.MODEL_TYPE,
InvenTreeManufacturerPart.MODEL_TYPE,
InvenTreePart.MODEL_TYPE,
InvenTreeStockLocation.MODEL_TYPE,
InvenTreeCompany.MODEL_TYPE,
];
if (InvenTreeAPI().supportsOrderBarcodes) {
validModels.add(InvenTreePurchaseOrder.MODEL_TYPE);
validModels.add(InvenTreeSalesOrder.MODEL_TYPE);
}
for (var key in validModels) {
if (data.containsKey(key)) {
try {
pk = (data[key]?["pk"] ?? -1) as int;
// Break on the first valid match found
if (pk > 0) {
model = key;
break;
}
} catch (error, stackTrace) {
sentryReportError("onBarcodeMatched", error, stackTrace);
}
}
}
// A valid result has been found
if (pk > 0 && model.isNotEmpty) {
barcodeSuccessTone();
switch (model) {
case InvenTreeStockItem.MODEL_TYPE:
await handleStockItem(pk);
return;
case InvenTreePurchaseOrder.MODEL_TYPE:
await handlePurchaseOrder(pk);
return;
case InvenTreeSalesOrder.MODEL_TYPE:
await handleSalesOrder(pk);
return;
case InvenTreeStockLocation.MODEL_TYPE:
await handleStockLocation(pk);
return;
case InvenTreeSupplierPart.MODEL_TYPE:
await handleSupplierPart(pk);
return;
case InvenTreeManufacturerPart.MODEL_TYPE:
await handleManufacturerPart(pk);
return;
case InvenTreePart.MODEL_TYPE:
await handlePart(pk);
return;
case InvenTreeCompany.MODEL_TYPE:
await handleCompany(pk);
return;
default:
// Fall through to failure state
break;
}
}
// If we get here, we have not found a valid barcode result!
barcodeFailureTone();
showSnackIcon(
L10().barcodeUnknown,
success: false,
onAction: () {
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) => SimpleDialog(
title: Text(L10().unknownResponse),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(data.toString()),
),
],
),
);
}
},
);
}
}
/*
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
*/
class UniqueBarcodeHandler extends BarcodeHandler {
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
// Callback function when a "unique" barcode hash is found
final Function(String) callback;
final String overlayText;
@override
String getOverlayText(BuildContext context) {
if (overlayText.isEmpty) {
return L10().barcodeScanAssign;
} else {
return overlayText;
}
}
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) {
showServerError("barcode/", L10().missingData, L10().barcodeMissingHash);
} else {
String barcode;
barcode = (data["barcode_data"] ?? "") as String;
if (barcode.isEmpty) {
barcodeFailureTone();
showSnackIcon(L10().barcodeError, success: false);
} else {
barcodeSuccessTone();
// Close the barcode scanner
if (OneContext.hasContext) {
OneContext().pop();
}
callback(barcode);
}
}
}
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
await onBarcodeMatched(data);
}
}
SpeedDialChild customBarcodeAction(
BuildContext context,
RefreshableState state,
String barcode,
String model,
int pk,
) {
if (barcode.isEmpty) {
return SpeedDialChild(
label: L10().barcodeAssign,
child: Icon(Icons.barcode_reader),
onTap: () {
var handler = UniqueBarcodeHandler((String barcode) {
InvenTreeAPI()
.linkBarcode({model: pk.toString(), "barcode": barcode})
.then((bool result) {
showSnackIcon(
result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
success: result,
);
state.refresh(context);
});
});
scanBarcode(context, handler: handler);
},
);
} else {
return SpeedDialChild(
child: Icon(Icons.barcode_reader),
label: L10().barcodeUnassign,
onTap: () {
InvenTreeAPI().unlinkBarcode({model: pk.toString()}).then((
bool result,
) {
showSnackIcon(
result ? L10().requestSuccessful : L10().requestFailed,
success: result,
);
state.refresh(context);
});
},
);
}
}

View file

@ -1,398 +0,0 @@
import "dart:math";
import "package:camera/camera.dart";
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/snacks.dart";
import "package:mobile_scanner/mobile_scanner.dart";
import "package:one_context/one_context.dart";
import "package:wakelock_plus/wakelock_plus.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/barcode/controller.dart";
/*
* Barcode controller which uses the device's camera to scan barcodes.
* Under the hood it uses the qr_code_scanner package.
*/
class CameraBarcodeController extends InvenTreeBarcodeController {
const CameraBarcodeController(BarcodeHandler handler, {Key? key})
: super(handler, key: key);
@override
State<StatefulWidget> createState() => _CameraBarcodeControllerState();
}
class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
_CameraBarcodeControllerState() : super();
bool flash_status = false;
int scan_delay = 500;
bool single_scanning = false;
bool scanning_paused = false;
bool multiple_barcodes = false;
String scanned_code = "";
double zoomFactor = 0.0;
final MobileScannerController controller = MobileScannerController(
autoZoom: false, // Disable autoZoom as we implement a manual slider
);
@override
void initState() {
super.initState();
_loadSettings();
WakelockPlus.enable();
}
@override
void dispose() {
super.dispose();
controller.dispose();
WakelockPlus.disable();
}
/*
* Load the barcode scanning settings
*/
Future<void> _loadSettings() async {
bool _single = await InvenTreeSettingsManager().getBool(
INV_BARCODE_SCAN_SINGLE,
false,
);
int _delay =
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
as int;
if (mounted) {
setState(() {
scan_delay = _delay;
single_scanning = _single;
scanning_paused = false;
});
}
}
@override
Future<void> pauseScan() async {
if (mounted) {
setState(() {
scanning_paused = true;
});
}
}
@override
Future<void> resumeScan() async {
controller.start();
if (mounted) {
setState(() {
scanning_paused = false;
});
}
}
/*
* Callback function when a barcode is scanned
*/
Future<void> onScanSuccess(BarcodeCapture result) async {
if (!mounted || scanning_paused) {
return;
}
// TODO: Display outline of barcodes on the screen?
if (result.barcodes.isEmpty) {
setState(() {
multiple_barcodes = false;
});
} else if (result.barcodes.length > 1) {
setState(() {
multiple_barcodes = true;
});
return;
} else {
setState(() {
multiple_barcodes = false;
});
}
String barcode = result.barcodes.first.rawValue ?? "";
if (barcode.isEmpty) {
// TODO: Error message "empty barcode"
return;
}
setState(() {
scanned_code = barcode;
});
pauseScan();
await handleBarcodeData(barcode).then((_) {
if (!single_scanning && mounted) {
resumeScan();
}
});
resumeScan();
if (mounted) {
setState(() {
scanned_code = "";
multiple_barcodes = false;
});
}
}
void onControllerCreated(CameraController? controller, Exception? error) {
if (error != null) {
sentryReportError(
"CameraBarcodeController.onControllerCreated",
error,
null,
);
}
if (controller == null) {
showSnackIcon(
L10().cameraCreationError,
icon: TablerIcons.camera_x,
success: false,
);
if (OneContext.hasContext) {
Navigator.pop(OneContext().context!);
}
}
}
Widget BarcodeOverlay(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
final double width = screenSize.width;
final double height = screenSize.height;
final double D = min(width, height) * 0.8;
// Color for the barcode scan?
Color overlayColor = COLOR_ACTION;
if (multiple_barcodes) {
overlayColor = COLOR_DANGER;
} else if (scanned_code.isNotEmpty) {
overlayColor = COLOR_SUCCESS;
} else if (scanning_paused) {
overlayColor = COLOR_WARNING;
}
return Stack(
children: [
Center(
child: Container(
width: D,
height: D,
decoration: BoxDecoration(
border: Border.all(color: overlayColor, width: 4),
),
),
),
],
);
}
/*
* Build the barcode reader widget
*/
Widget BarcodeReader(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
final double width = screenSize.width;
final double height = screenSize.height;
final double D = min(width, height) * 0.8;
return MobileScanner(
controller: controller,
overlayBuilder: (context, constraints) {
return BarcodeOverlay(context);
},
scanWindow: Rect.fromCenter(
center: Offset(width / 2, height / 2),
width: D,
height: D,
),
onDetect: (result) {
onScanSuccess(result);
},
);
}
Widget topCenterOverlay() {
return SafeArea(
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 75, bottom: 10),
child: Text(
widget.handler.getOverlayText(context),
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
Widget bottomCenterOverlay() {
String info_text = scanning_paused
? L10().barcodeScanPaused
: L10().barcodeScanPause;
String text = scanned_code.isNotEmpty ? scanned_code : info_text;
if (text.length > 50) {
text = text.substring(0, 50) + "...";
}
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 75),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
Widget? buildActions(BuildContext context) {
List<SpeedDialChild> actions = [
SpeedDialChild(
child: Icon(flash_status ? TablerIcons.bulb_off : TablerIcons.bulb),
label: L10().toggleTorch,
onTap: () async {
controller.toggleTorch();
if (mounted) {
setState(() {
flash_status = !flash_status;
});
}
},
),
SpeedDialChild(
child: Icon(TablerIcons.camera),
label: L10().switchCamera,
onTap: () async {
controller.switchCamera();
},
),
];
return SpeedDial(icon: Icons.more_horiz, children: actions);
}
Widget zoomSlider() {
return Positioned(
left: 0,
right: 0,
bottom: 16,
child: Center(
child: Container(
width: 225,
height: 56,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(28),
),
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(TablerIcons.zoom_out, color: Colors.white, size: 20),
Expanded(
child: Slider(
value: zoomFactor,
min: 0.0,
max: 1.0,
activeColor: Colors.white,
inactiveColor: Colors.white.withValues(alpha: 0.3),
onChanged: (value) {
setState(() {
zoomFactor = value;
controller.setZoomScale(value);
});
},
onChangeStart: (value) async {
if (mounted) {
setState(() {
scanning_paused = true;
});
}
},
onChangeEnd: (value) async {
if (mounted) {
setState(() {
scanning_paused = false;
});
}
},
),
),
Icon(TablerIcons.zoom_in, color: Colors.white, size: 20),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: COLOR_APP_BAR,
title: Text(L10().scanBarcode),
),
floatingActionButton: buildActions(context),
body: GestureDetector(
onTap: () async {
if (mounted) {
setState(() {
// Toggle the 'scan paused' state
scanning_paused = !scanning_paused;
});
}
},
child: Stack(
children: <Widget>[
Column(children: [Expanded(child: BarcodeReader(context))]),
topCenterOverlay(),
bottomCenterOverlay(),
zoomSlider(),
],
),
),
);
}
}

View file

@ -1,103 +0,0 @@
import "package:flutter/material.dart";
import "package:inventree/preferences.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/widget/progress.dart";
/*
* Generic class which provides a barcode scanner interface.
*
* When the controller is instantiated, it is passed a "handler" class,
* which is used to process the scanned barcode.
*/
class InvenTreeBarcodeController extends StatefulWidget {
const InvenTreeBarcodeController(this.handler, {Key? key}) : super(key: key);
final BarcodeHandler handler;
@override
State<StatefulWidget> createState() => InvenTreeBarcodeControllerState();
}
/*
* Base state widget for the barcode controller.
* This defines the basic interface for the barcode controller.
*/
class InvenTreeBarcodeControllerState
extends State<InvenTreeBarcodeController> {
InvenTreeBarcodeControllerState() : super();
final GlobalKey barcodeControllerKey = GlobalKey(
debugLabel: "barcodeController",
);
// Internal state flag to test if we are currently processing a barcode
bool processingBarcode = false;
/*
* Method to handle scanned data.
* Any implementing class should call this method when a barcode is scanned.
* Barcode data should be passed as a string
*/
Future<void> handleBarcodeData(String? data) async {
// Check that the data is valid, and this view is still mounted
if (!mounted || data == null || data.isEmpty) {
return;
}
// Currently processing a barcode - ignore this one
if (processingBarcode) {
return;
}
setState(() {
processingBarcode = true;
});
showLoadingOverlay();
await pauseScan();
await widget.handler.processBarcode(data);
// processBarcode may have popped the context
if (!mounted) {
hideLoadingOverlay();
return;
}
int delay =
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
as int;
Future.delayed(Duration(milliseconds: delay), () {
hideLoadingOverlay();
if (mounted) {
resumeScan().then((_) {
if (mounted) {
setState(() {
processingBarcode = false;
});
}
});
}
});
}
// Hook function to "pause" the barcode scanner
Future<void> pauseScan() async {
// Implement this function in subclass
}
// Hook function to "resume" the barcode scanner
Future<void> resumeScan() async {
// Implement this function in subclass
}
/*
* Implementing classes are in control of building out the widget
*/
@override
Widget build(BuildContext context) {
return Container();
}
}

View file

@ -1,130 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/tones.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
/* Generic class which "handles" a barcode, by communicating with the InvenTree server,
* and handling match / unknown / error cases.
*
* Override functionality of this class to perform custom actions,
* based on the response returned from the InvenTree server
*/
class BarcodeHandler {
BarcodeHandler();
// Return the text to display on the barcode overlay
// Note: Will be overridden by child classes
String getOverlayText(BuildContext context) => "Barcode Overlay";
// Called when the server "matches" a barcode
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// Override this function
}
// Called when the server does not know about a barcode
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
// Override this function
barcodeFailureTone();
showSnackIcon(
(data["error"] ?? L10().barcodeNoMatch) as String,
success: false,
icon: Icons.qr_code,
);
}
// Called when the server returns an unhandled response
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
barcodeFailureTone();
showServerError("barcode/", L10().responseUnknown, data.toString());
}
/*
* Base function to capture and process barcode data.
*
* Returns true only if the barcode scanner should remain open
*/
Future<void> processBarcode(
String barcode, {
String url = "barcode/",
Map<String, dynamic> extra_data = const {},
}) async {
debug("Scanned barcode data: '${barcode}'");
barcode = barcode.trim();
// Empty barcode is invalid
if (barcode.isEmpty) {
barcodeFailureTone();
showSnackIcon(
L10().barcodeError,
icon: TablerIcons.exclamation_circle,
success: false,
);
return;
}
APIResponse? response;
try {
response = await InvenTreeAPI().post(
url,
body: {"barcode": barcode, ...extra_data},
expectedStatusCode: null, // Do not show an error on "unexpected code"
);
} catch (error, stackTrace) {
sentryReportError("Barcode.processBarcode", error, stackTrace);
response = null;
}
if (response == null) {
barcodeFailureTone();
showSnackIcon(L10().barcodeError, success: false);
return;
}
debug("Barcode scan response" + response.data.toString());
Map<String, dynamic> data = response.asMap();
// Handle strange response from the server
if (!response.isValid() || !response.isMap()) {
await onBarcodeUnknown({});
showSnackIcon(L10().serverError, success: false);
// We want to know about this one!
await sentryReportMessage(
"BarcodeHandler.processBarcode returned unexpected value",
context: {
"data": response.data?.toString() ?? "null",
"barcode": barcode,
"url": url,
"statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(),
"error": response.error,
"errorDetail": response.errorDetail,
"className": "${this}",
},
);
} else if (data.containsKey("success")) {
await onBarcodeMatched(data);
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
await onBarcodeUnknown(data);
} else {
await onBarcodeUnhandled(data);
}
}
}

View file

@ -1,194 +0,0 @@
import "package:flutter/material.dart";
import "package:inventree/preferences.dart";
import "package:one_context/one_context.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/barcode/tones.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/snacks.dart";
/*
* Barcode handler class for scanning a supplier barcode to receive a part
*
* - The class can be initialized by optionally passing a valid, placed PurchaseOrder object
* - Expects to scan supplier barcode, possibly containing order_number and quantity
* - If location or quantity information wasn't provided, show a form to fill it in
*/
class POReceiveBarcodeHandler extends BarcodeHandler {
POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem});
InvenTreePurchaseOrder? purchaseOrder;
InvenTreeStockLocation? location;
InvenTreePOLineItem? lineItem;
@override
String getOverlayText(BuildContext context) => L10().barcodeReceivePart;
@override
Future<void> processBarcode(
String barcode, {
String url = "barcode/po-receive/",
Map<String, dynamic> extra_data = const {},
}) async {
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_PO_CONFIRM_SCAN,
true,
);
final po_extra_data = {
"purchase_order": purchaseOrder?.pk,
"location": location?.pk,
"line_item": lineItem?.pk,
"auto_allocate": !confirm,
...extra_data,
};
return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
}
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (data.containsKey("lineitem") || data.containsKey("success")) {
barcodeSuccess(L10().receivedItem);
return;
} else {
return onBarcodeUnknown(data);
}
}
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
if (!data.containsKey("action_required") || !data.containsKey("lineitem")) {
return super.onBarcodeUnhandled(data);
}
final lineItemData = data["lineitem"] as Map<String, dynamic>;
if (!lineItemData.containsKey("pk") ||
!lineItemData.containsKey("purchase_order")) {
barcodeFailureTone();
showSnackIcon(L10().missingData, success: false);
}
// At minimum, we need the line item ID value
final int? lineItemId = lineItemData["pk"] as int?;
if (lineItemId == null) {
barcodeFailureTone();
return;
}
InvenTreePOLineItem? lineItem =
await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?;
if (lineItem == null) {
barcodeFailureTone();
return;
}
// Next, extract the "optional" fields
// Extract information from the returned server response
double? quantity = double.tryParse(
(lineItemData["quantity"] ?? "0").toString(),
);
int? destination = lineItemData["location"] as int?;
String? barcode = data["barcode_data"] as String?;
// Discard the barcode scanner at this stage
if (OneContext.hasContext) {
OneContext().pop();
}
await lineItem.receive(
OneContext().context!,
destination: destination,
quantity: quantity,
barcode: barcode,
onSuccess: () {
showSnackIcon(L10().receivedItem, success: true);
},
);
}
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
data["error"] as String? ?? L10().barcodeError,
success: false,
);
}
}
/*
* Barcode handler to add a line item to a purchase order
*/
class POAllocateBarcodeHandler extends BarcodeHandler {
POAllocateBarcodeHandler({this.purchaseOrder});
InvenTreePurchaseOrder? purchaseOrder;
@override
String getOverlayText(BuildContext context) => L10().scanSupplierPart;
@override
Future<void> processBarcode(
String barcode, {
String url = "barcode/po-allocate/",
Map<String, dynamic> extra_data = const {},
}) {
final po_extra_data = {"purchase_order": purchaseOrder?.pk, ...extra_data};
return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
}
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// Server must respond with a suppliertpart instance
if (!data.containsKey("supplierpart")) {
return onBarcodeUnknown(data);
}
dynamic supplier_part = data["supplierpart"];
int supplier_part_pk = -1;
if (supplier_part is Map<String, dynamic>) {
supplier_part_pk = (supplier_part["pk"] ?? -1) as int;
} else {
return onBarcodeUnknown(data);
}
// Dispose of the barcode scanner
if (OneContext.hasContext) {
OneContext().pop();
}
final context = OneContext().context!;
var fields = InvenTreePOLineItem().formFields();
fields["order"]?["value"] = purchaseOrder!.pk;
fields["part"]?["hidden"] = false;
fields["part"]?["value"] = supplier_part_pk;
InvenTreePOLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
);
}
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
print("onBarcodeUnhandled:");
print(data.toString());
super.onBarcodeUnhandled(data);
}
}

View file

@ -1,163 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api_form.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/sales_order.dart";
import "package:one_context/one_context.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/barcode/tones.dart";
import "package:inventree/widget/snacks.dart";
/*
* Barcode handler class for scanning a new part into a SalesOrder
*/
class SOAddItemBarcodeHandler extends BarcodeHandler {
SOAddItemBarcodeHandler({this.salesOrder});
InvenTreeSalesOrder? salesOrder;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanPart;
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// Extract the part ID from the returned data
int part_id = -1;
if (data.containsKey("part")) {
part_id = (data["part"] ?? {} as Map<String, dynamic>)["pk"] as int;
}
if (part_id <= 0) {
return onBarcodeUnknown(data);
}
// Request the part from the server
var part = await InvenTreePart().get(part_id);
if (part is InvenTreePart) {
if (part.isSalable) {
// Dispose of the barcode scanner
if (OneContext.hasContext) {
OneContext().pop();
}
final context = OneContext().context!;
var fields = InvenTreeSOLineItem().formFields();
fields["order"]?["value"] = salesOrder!.pk;
fields["order"]?["hidden"] = true;
fields["part"]?["value"] = part.pk;
fields["part"]?["hidden"] = false;
InvenTreeSOLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
);
} else {
barcodeFailureTone();
showSnackIcon(L10().partNotSalable, success: false);
}
} else {
// Failed to fetch part
return onBarcodeUnknown(data);
}
}
}
class SOAllocateStockHandler extends BarcodeHandler {
SOAllocateStockHandler({this.salesOrder, this.lineItem, this.shipment});
InvenTreeSalesOrder? salesOrder;
InvenTreeSOLineItem? lineItem;
InvenTreeSalesOrderShipment? shipment;
@override
String getOverlayText(BuildContext context) => L10().allocateStock;
@override
Future<void> processBarcode(
String barcode, {
String url = "barcode/so-allocate/",
Map<String, dynamic> extra_data = const {},
}) {
final so_extra_data = {
"sales_order": salesOrder?.pk,
"shipment": shipment?.pk,
"line": lineItem?.pk,
...extra_data,
};
return super.processBarcode(barcode, url: url, extra_data: so_extra_data);
}
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("line_item")) {
return onBarcodeUnknown(data);
}
barcodeSuccess(L10().allocated);
}
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
if (!data.containsKey("action_required") ||
!data.containsKey("line_item")) {
return super.onBarcodeUnhandled(data);
}
// Prompt user for extra information to create the allocation
var fields = InvenTreeSOLineItem().allocateFormFields();
// Update fields with data gathered from the API response
fields["line_item"]?["value"] = data["line_item"];
Map<String, dynamic> stock_filters = {"in_stock": true, "available": true};
if (data.containsKey("part")) {
stock_filters["part"] = data["part"];
}
fields["stock_item"]?["filters"] = stock_filters;
fields["stock_item"]?["value"] = data["stock_item"];
fields["quantity"]?["value"] = data["quantity"];
fields["shipment"]?["value"] = data["shipment"];
fields["shipment"]?["filters"] = {"order": salesOrder!.pk.toString()};
final context = OneContext().context!;
launchApiForm(
context,
L10().allocateStock,
salesOrder!.allocate_url,
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) async {
showSnackIcon(L10().allocated, success: true);
},
);
}
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
data["error"] as String? ?? L10().barcodeError,
success: false,
);
}
}

View file

@ -1,270 +0,0 @@
import "package:flutter/cupertino.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api_form.dart";
import "package:inventree/preferences.dart";
import "package:one_context/one_context.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/barcode/tones.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
/*
* Generic class for scanning a StockLocation.
*
* - Validates that the scanned barcode matches a valid StockLocation
* - Runs a "callback" function if a valid StockLocation is found
*/
class BarcodeScanStockLocationHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// We expect that the barcode points to a 'stocklocation'
if (data.containsKey("stocklocation")) {
int _loc = (data["stocklocation"]?["pk"] ?? -1) as int;
// A valid stock location!
if (_loc > 0) {
debug("Scanned stock location ${_loc}");
final bool result = await onLocationScanned(_loc);
if (result && hasContext()) {
OneContext().pop();
}
return;
}
}
// If we get to this point, something went wrong during the scan process
barcodeFailureTone();
showSnackIcon(L10().invalidStockLocation, success: false);
}
// Callback function which runs when a valid StockLocation is scanned
// If this function returns 'true' the barcode scanning dialog will be closed
Future<bool> onLocationScanned(int locationId) async {
// Re-implement this for particular subclass
return false;
}
}
/*
* Generic class for scanning a StockItem
*
* - Validates that the scanned barcode matches a valid StockItem
* - Runs a "callback" function if a valid StockItem is found
*/
class BarcodeScanStockItemHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// We expect that the barcode points to a 'stockitem'
if (data.containsKey("stockitem")) {
int _item = (data["stockitem"]?["pk"] ?? -1) as int;
// A valid stock location!
if (_item > 0) {
barcodeSuccessTone();
bool result = await onItemScanned(_item);
if (result && OneContext.hasContext) {
OneContext().pop();
return;
}
}
}
// If we get to this point, something went wrong during the scan process
barcodeFailureTone();
showSnackIcon(L10().invalidStockItem, success: false);
}
// Callback function which runs when a valid StockItem is scanned
Future<bool> onItemScanned(int itemId) async {
// Re-implement this for particular subclass
return false;
}
}
/*
* Barcode handler for scanning a provided StockItem into a scanned StockLocation.
*
* - The class is initialized by passing a valid StockItem object
* - Expects to scan barcode for a StockLocation
* - The StockItem is transferred into the scanned location
*/
class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
StockItemScanIntoLocationHandler(this.item);
final InvenTreeStockItem item;
@override
Future<bool> onLocationScanned(int locationId) async {
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_STOCK_CONFIRM_SCAN,
false,
);
bool result = false;
if (confirm) {
Map<String, dynamic> fields = item.transferFields();
// Override location with scanned value
fields["location"]?["value"] = locationId;
launchApiForm(
OneContext().context!,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
showSnackIcon(L10().stockItemUpdated, success: true);
},
);
return true;
} else {
result = await item.transferStock(locationId);
}
if (result) {
barcodeSuccess(L10().barcodeScanIntoLocationSuccess);
} else {
barcodeFailureTone();
showSnackIcon(L10().barcodeScanIntoLocationFailure, success: false);
}
return result;
}
}
/*
* Barcode handler for scanning stock item(s) into the specified StockLocation.
*
* - The class is initialized by passing a valid StockLocation object
* - Expects to scan a barcode for a StockItem
* - The scanned StockItem is transferred into the provided StockLocation
*/
class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
StockLocationScanInItemsHandler(this.location);
final InvenTreeStockLocation location;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@override
Future<bool> onItemScanned(int itemId) async {
final InvenTreeStockItem? item =
await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_STOCK_CONFIRM_SCAN,
false,
);
bool result = false;
if (item != null) {
// Item is already *in* the specified location
if (item.locationId == location.pk) {
barcodeFailureTone();
showSnackIcon(L10().itemInLocation, success: true);
return false;
} else {
if (confirm) {
Map<String, dynamic> fields = item.transferFields();
// Override location with provided location value
fields["location"]?["value"] = location.pk;
launchApiForm(
OneContext().context!,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
showSnackIcon(L10().stockItemUpdated, success: true);
},
);
return true;
} else {
result = await item.transferStock(location.pk);
showSnackIcon(
result
? L10().barcodeScanIntoLocationSuccess
: L10().barcodeScanIntoLocationFailure,
success: result,
);
}
}
}
// We always return false here, to ensure the barcode scan dialog remains open
return false;
}
}
/*
* Barcode handler class for scanning a StockLocation into another StockLocation
*
* - The class is initialized by passing a valid StockLocation object
* - Expects to scan barcode for another *parent* StockLocation
* - The scanned StockLocation is set as the "parent" of the provided StockLocation
*/
class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
ScanParentLocationHandler(this.location);
final InvenTreeStockLocation location;
@override
Future<bool> onLocationScanned(int locationId) async {
final response = await location.update(
values: {"parent": locationId.toString()},
expectedStatusCode: null,
);
switch (response.statusCode) {
case 200:
case 201:
barcodeSuccess(L10().barcodeScanIntoLocationSuccess);
return true;
case 400: // Invalid parent location chosen
barcodeFailureTone();
showSnackIcon(L10().invalidStockLocation, success: false);
return false;
default:
barcodeFailureTone();
showSnackIcon(
L10().barcodeScanIntoLocationFailure,
success: false,
actionText: L10().details,
onAction: () {
showErrorDialog(L10().barcodeError, response: response);
},
);
return false;
}
}
}

View file

@ -1,25 +0,0 @@
import "package:inventree/helpers.dart";
import "package:inventree/preferences.dart";
/*
* Play an audible 'success' alert to the user.
*/
Future<void> barcodeSuccessTone() async {
final bool en =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
as bool;
if (en) {
playAudioFile("sounds/barcode_scan.mp3");
}
}
Future<void> barcodeFailureTone() async {
final bool en =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
as bool;
if (en) {
playAudioFile("sounds/barcode_error.mp3");
}
}

View file

@ -1,146 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/barcode/controller.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
/*
* Barcode controller which acts as a keyboard wedge,
* intercepting barcode data which is entered as rapid keyboard presses
*/
class WedgeBarcodeController extends InvenTreeBarcodeController {
const WedgeBarcodeController(BarcodeHandler handler, {Key? key})
: super(handler, key: key);
@override
State<StatefulWidget> createState() => _WedgeBarcodeControllerState();
}
class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
_WedgeBarcodeControllerState() : super();
bool canScan = true;
bool get scanning => mounted && canScan;
final FocusNode _focusNode = FocusNode();
List<String> _scannedCharacters = [];
DateTime? _lastScanTime;
@override
Future<void> pauseScan() async {
if (mounted) {
setState(() {
canScan = false;
});
}
}
@override
Future<void> resumeScan() async {
if (mounted) {
setState(() {
canScan = true;
});
}
}
// Callback for a single key press / scan
void handleKeyEvent(KeyEvent event) {
if (!scanning) {
return;
}
// Look only for key-down events
if (event is! KeyDownEvent) {
return;
}
// Ignore events without a character code
if (event.character == null) {
return;
}
DateTime now = DateTime.now();
// Throw away old characters
if (_lastScanTime == null ||
_lastScanTime!.isBefore(now.subtract(Duration(milliseconds: 250)))) {
_scannedCharacters.clear();
}
_lastScanTime = now;
if (event.character == "\n") {
if (_scannedCharacters.isNotEmpty) {
// Debug output required for unit testing
debug("scanned: ${_scannedCharacters.join()}");
handleBarcodeData(_scannedCharacters.join());
}
_scannedCharacters.clear();
} else {
_scannedCharacters.add(event.character!);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: COLOR_APP_BAR,
title: Text(L10().scanBarcode),
),
backgroundColor: Colors.black.withValues(alpha: 0.9),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(flex: 5),
Icon(TablerIcons.barcode, size: 64),
Spacer(flex: 5),
KeyboardListener(
autofocus: true,
focusNode: _focusNode,
child: SizedBox(
child: CircularProgressIndicator(
color: scanning ? COLOR_ACTION : COLOR_PROGRESS,
),
width: 64,
height: 64,
),
onKeyEvent: (event) {
handleKeyEvent(event);
},
// onBarcodeScanned: (String barcode) {
// debug("scanned: ${barcode}");
// if (scanning) {
// // Process the barcode data
// handleBarcodeData(barcode);
// }
// },
),
Spacer(flex: 5),
Padding(
child: Text(
widget.handler.getOverlayText(context),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
padding: EdgeInsets.all(20),
),
],
),
),
);
}
}

View file

@ -1,7 +0,0 @@
/*
* For integration with sentry.io, fill out the SENTRY_DSN_KEY value below.
* This should be set to a valid DSN key, from your sentry.io account
*
*/
String SENTRY_DSN_KEY =
"https://fea705aa4b8e4c598dcf9b146b3d1b86@o378676.ingest.sentry.io/5202450";

3
lib/dummy_dsn.dart Normal file
View file

@ -0,0 +1,3 @@
// Dummy DSN to use for unit testing, etc
const String SENTRY_DSN_KEY = "https://12345678901234567890@abcdef.ingest.sentry.io/11223344";

76
lib/generated/i18n.dart Normal file
View file

@ -0,0 +1,76 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: camel_case_types
// ignore_for_file: prefer_single_quotes
//This file is automatically generated. DO NOT EDIT, all your changes would be lost.
class S implements WidgetsLocalizations {
const S();
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
@override
TextDirection get textDirection => TextDirection.ltr;
}
class en extends S {
const en();
}
class GeneratedLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
const GeneratedLocalizationsDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
const Locale("en", ""),
];
}
LocaleResolutionCallback resolution({Locale fallback}) {
return (Locale locale, Iterable<Locale> supported) {
final Locale languageLocale = new Locale(locale.languageCode, "");
if (supported.contains(locale))
return locale;
else if (supported.contains(languageLocale))
return languageLocale;
else {
final Locale fallbackLocale = fallback ?? supported.first;
return fallbackLocale;
}
};
}
@override
Future<WidgetsLocalizations> load(Locale locale) {
final String lang = getLang(locale);
switch (lang) {
case "en":
return new SynchronousFuture<WidgetsLocalizations>(const en());
default:
return new SynchronousFuture<WidgetsLocalizations>(const S());
}
}
@override
bool isSupported(Locale locale) => supportedLocales.contains(locale);
@override
bool shouldReload(GeneratedLocalizationsDelegate old) => false;
}
String getLang(Locale l) => l.countryCode != null && l.countryCode.isEmpty
? l.languageCode
: l.toString();

View file

@ -7,173 +7,31 @@
* supressing trailing zeroes * supressing trailing zeroes
*/ */
import "dart:io";
import "package:currency_formatter/currency_formatter.dart";
import "package:one_context/one_context.dart";
import "package:url_launcher/url_launcher.dart";
import "package:audioplayers/audioplayers.dart"; import "package:audioplayers/audioplayers.dart";
import "package:inventree/app_settings.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/snacks.dart";
List<String> debug_messages = [];
void clearDebugMessage() => debug_messages.clear();
int debugMessageCount() {
print("Debug Messages: ${debug_messages.length}");
return debug_messages.length;
}
// Check if the debug log contains a given message
bool debugContains(String msg, {bool raiseAssert = true}) {
bool result = false;
for (String element in debug_messages) {
if (element.contains(msg)) {
result = true;
break;
}
}
if (!result) {
print("Debug does not contain expected string: '${msg}'");
}
if (raiseAssert) {
assert(result);
}
return result;
}
bool isTesting() {
return Platform.environment.containsKey("FLUTTER_TEST");
}
bool hasContext() {
try {
return !isTesting() && OneContext.hasContext;
} catch (error) {
return false;
}
}
/*
* Display a debug message if we are in testing mode, or running in debug mode
*/
void debug(dynamic msg) {
if (Platform.environment.containsKey("FLUTTER_TEST")) {
debug_messages.add(msg.toString());
}
print("DEBUG: ${msg.toString()}");
}
/*
* Simplify string representation of a floating point value
* Basically, don't display fractional component if it is an integer
*/
String simpleNumberString(double number) { String simpleNumberString(double number) {
if (number.toInt() == number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart
return number.toInt().toString();
} else { return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1);
return number.toString(); }
Future<void> successTone() async {
final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_scan.mp3");
} }
} }
/* Future <void> failureTone() async {
* Play an audio file from the requested path.
*
* Note: If OneContext module fails the 'hasContext' check,
* we will not attempt to play the sound
*/
Future<void> playAudioFile(String path) async {
// Debug message for unit testing
debug("Playing audio file: '${path}'");
if (!hasContext()) { final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
return;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_error.mp3");
} }
}
final player = AudioPlayer();
// Specify context options for the audio player
// Ref: https://github.com/inventree/inventree-app/issues/582
player.setAudioContext(
AudioContext(
android: AudioContextAndroid(
usageType: AndroidUsageType.notification,
audioFocus: AndroidAudioFocus.none,
),
iOS: AudioContextIOS(),
),
);
player.play(AssetSource(path));
}
// Open an external URL
Future<void> openLink(String url) async {
final link = Uri.parse(url);
try {
await launchUrl(link);
} catch (e) {
showSnackIcon(L10().error, success: false);
}
}
/*
* Helper function for rendering a money / currency object as a String
*/
String renderCurrency(double? amount, String currency, {int decimals = 2}) {
if (amount == null || amount.isInfinite || amount.isNaN) return "-";
currency = currency.trim();
if (currency.isEmpty) return "-";
CurrencyFormat fmt =
CurrencyFormat.fromCode(currency.toLowerCase()) ?? CurrencyFormat.usd;
String value = CurrencyFormatter.format(amount, fmt);
return value;
}
bool isValidNumber(double? value) {
return value != null && !value.isNaN && !value.isInfinite;
}
/*
* Render a "range" of prices between two values.
*/
String formatPriceRange(
double? minPrice,
double? maxPrice, {
String? currency,
}) {
// Account for empty or null values
if (!isValidNumber(minPrice) && !isValidNumber(maxPrice)) {
return "-";
}
if (isValidNumber(minPrice) && isValidNumber(maxPrice)) {
// Two values are equal
if (minPrice == maxPrice) {
return renderCurrency(minPrice, currency ?? "USD");
} else {
return "${renderCurrency(minPrice, currency ?? "USD")} - ${renderCurrency(maxPrice, currency ?? "USD")}";
}
}
if (isValidNumber(minPrice)) {
return renderCurrency(minPrice, currency ?? "USD");
} else if (isValidNumber(maxPrice)) {
return renderCurrency(maxPrice, currency ?? "USD");
} else {
return "-";
}
}

View file

@ -1,176 +0,0 @@
import "dart:io";
import "package:flutter/cupertino.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/snacks.dart";
import "package:path/path.dart" as path;
class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file
InvenTreeAttachment() : super();
InvenTreeAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeAttachment createFromJson(Map<String, dynamic> json) =>
InvenTreeAttachment.fromJson(json);
@override
String get URL => "attachment/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {"link": {}, "comment": {}};
if (!hasLink) {
fields.remove("link");
}
return fields;
}
// The model type of the instance this attachment is associated with
String get modelType => getString("model_type");
// The ID of the instance this attachment is associated with
int get modelId => getInt("model_id");
String get attachment => getString("attachment");
bool get hasAttachment => attachment.isNotEmpty;
// Return the filename of the attachment
String get filename {
return attachment.split("/").last;
}
IconData get icon {
String fn = filename.toLowerCase();
if (fn.endsWith(".pdf")) {
return TablerIcons.file_type_pdf;
} else if (fn.endsWith(".csv")) {
return TablerIcons.file_type_csv;
} else if (fn.endsWith(".doc") || fn.endsWith(".docx")) {
return TablerIcons.file_type_doc;
} else if (fn.endsWith(".xls") || fn.endsWith(".xlsx")) {
return TablerIcons.file_type_xls;
}
// Image formats
final List<String> img_formats = [".png", ".jpg", ".gif", ".bmp", ".svg"];
for (String fmt in img_formats) {
if (fn.endsWith(fmt)) {
return TablerIcons.file_type_jpg;
}
}
return TablerIcons.file;
}
String get comment => getString("comment");
DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
} else {
return null;
}
}
// Return a count of how many attachments exist against the specified model ID
Future<int> countAttachments(String modelType, int modelId) async {
Map<String, String> filters = {};
if (!api.supportsModernAttachments) {
return 0;
}
filters["model_type"] = modelType;
filters["model_id"] = modelId.toString();
return count(filters: filters);
}
Future<bool> uploadAttachment(
File attachment,
String modelType,
int modelId, {
String comment = "",
Map<String, String> fields = const {},
}) async {
// Ensure that the correct reference field is set
Map<String, String> data = Map<String, String>.from(fields);
String url = URL;
if (comment.isNotEmpty) {
data["comment"] = comment;
}
data["model_type"] = modelType;
data["model_id"] = modelId.toString();
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
attachment,
method: "POST",
name: "attachment",
fields: data,
);
return response.successful();
}
Future<bool> uploadImage(
String modelType,
int modelId, {
String prefix = "InvenTree",
}) async {
bool result = false;
await FilePickerDialog.pickImageFromCamera().then((File? file) {
if (file != null) {
String dir = path.dirname(file.path);
String ext = path.extension(file.path);
String now = DateTime.now().toIso8601String().replaceAll(":", "-");
// Rename the file with a unique name
String filename = "${dir}/${prefix}_image_${now}${ext}";
try {
return file.rename(filename).then((File renamed) {
return uploadAttachment(renamed, modelType, modelId).then((
success,
) {
result = success;
showSnackIcon(
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
success: result,
);
});
});
} catch (error, stackTrace) {
sentryReportError("uploadImage", error, stackTrace);
showSnackIcon(L10().imageUploadFailure, success: false);
}
}
});
return result;
}
/*
* Download this attachment file
*/
Future<void> downloadAttachment() async {
await InvenTreeAPI().downloadFile(attachment);
}
}

View file

@ -1,63 +0,0 @@
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
/*
* Class representing the BomItem database model
*/
class InvenTreeBomItem extends InvenTreeModel {
InvenTreeBomItem() : super();
InvenTreeBomItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeBomItem.fromJson(json);
@override
String get URL => "bom/";
@override
Map<String, String> defaultFilters() {
return {
"sub_part_detail": "true",
"part_detail": "true",
"show_pricing": "false",
};
}
// Extract the 'reference' value associated with this BomItem
String get reference => getString("reference");
// Extract the 'quantity' value associated with this BomItem
double get quantity => getDouble("quantity");
// Extract the ID of the related part
int get partId => getInt("part");
// Return a Part instance for the referenced part
InvenTreePart? get part {
if (jsondata.containsKey("part_detail")) {
dynamic data = jsondata["part_detail"] ?? {};
if (data is Map<String, dynamic>) {
return InvenTreePart.fromJson(data);
}
}
return null;
}
// Return a Part instance for the referenced sub-part
InvenTreePart? get subPart {
if (jsondata.containsKey("sub_part_detail")) {
dynamic data = jsondata["sub_part_detail"] ?? {};
if (data is Map<String, dynamic>) {
return InvenTreePart.fromJson(data);
}
}
return null;
}
// Extract the ID of the related sub-part
int get subPartId => getInt("sub_part");
}

View file

@ -1,17 +1,16 @@
import "dart:async"; import "dart:async";
import "package:flutter/material.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/company/company_detail.dart";
import "package:inventree/widget/company/supplier_part_detail.dart";
/* /*
* The InvenTreeCompany class represents the Company model in the InvenTree database. * The InvenTreeCompany class repreents the Company model in the InvenTree database.
*/ */
class InvenTreeCompany extends InvenTreeModel { class InvenTreeCompany extends InvenTreeModel {
InvenTreeCompany() : super(); InvenTreeCompany() : super();
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json); InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@ -19,26 +18,9 @@ class InvenTreeCompany extends InvenTreeModel {
@override @override
String get URL => "company/"; String get URL => "company/";
static const String MODEL_TYPE = "company";
@override @override
Future<Object?> goToDetailPage(BuildContext context) async { Map<String, dynamic> formFields() {
return Navigator.push( return {
context,
MaterialPageRoute(builder: (context) => CompanyDetailWidget(this)),
);
}
@override
List<String> get rolesRequired => [
"purchase_order",
"sales_order",
"return_order",
];
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"name": {}, "name": {},
"description": {}, "description": {},
"website": {}, "website": {},
@ -47,52 +29,41 @@ class InvenTreeCompany extends InvenTreeModel {
"is_customer": {}, "is_customer": {},
"currency": {}, "currency": {},
}; };
if (InvenTreeAPI().supportsCompanyActiveStatus) {
fields["active"] = {};
}
return fields;
} }
String get image => String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
(jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage)
as String;
String get thumbnail => String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
(jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb)
as String;
String get website => getString("website"); String get website => (jsondata["website"] ?? "") as String;
String get phone => getString("phone"); String get phone => (jsondata["phone"] ?? "") as String;
String get email => getString("email"); String get email => (jsondata["email"] ?? "") as String;
bool get isSupplier => getBool("is_supplier"); bool get isSupplier => (jsondata["is_supplier"] ?? false) as bool;
bool get isManufacturer => getBool("is_manufacturer"); bool get isManufacturer => (jsondata["is_manufacturer"] ?? false) as bool;
bool get isCustomer => getBool("is_customer"); bool get isCustomer => (jsondata["is_customer"] ?? false) as bool;
bool get active => getBool("active", backup: true); int get partSuppliedCount => (jsondata["parts_supplied"] ?? 0) as int;
int get partSuppliedCount => getInt("part_supplied"); int get partManufacturedCount => (jsondata["parts_manufactured"] ?? 0) as int;
int get partManufacturedCount => getInt("parts_manufactured");
// Request a list of purchase orders against this company // Request a list of purchase orders against this company
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({ Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
bool? outstanding,
}) async { Map<String, String> filters = {
Map<String, String> filters = {"supplier": "${pk}"}; "supplier": "${pk}"
};
if (outstanding != null) { if (outstanding != null) {
filters["outstanding"] = outstanding ? "true" : "false"; filters["outstanding"] = outstanding ? "true" : "false";
} }
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list( final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
filters: filters, filters: filters
); );
List<InvenTreePurchaseOrder> orders = []; List<InvenTreePurchaseOrder> orders = [];
@ -107,189 +78,103 @@ class InvenTreeCompany extends InvenTreeModel {
} }
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreeCompany.fromJson(json); var company = InvenTreeCompany.fromJson(json);
return company;
}
} }
/* /*
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database * The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
*/ */
class InvenTreeSupplierPart extends InvenTreeModel { class InvenTreeSupplierPart extends InvenTreeModel {
InvenTreeSupplierPart() : super(); InvenTreeSupplierPart() : super();
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String get URL => "company/part/"; String get URL => "company/part/";
static const String MODEL_TYPE = "supplierpart"; Map<String, String> _filters() {
@override
List<String> get rolesRequired => ["part", "purchase_order"];
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => SupplierPartDetailWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"supplier": {},
"SKU": {},
"link": {},
"note": {},
"packaging": {},
};
// At some point, pack_size was changed to pack_quantity
if (InvenTreeAPI().apiVersion < 117) {
fields["pack_size"] = {};
} else {
fields["pack_quantity"] = {};
}
if (InvenTreeAPI().supportsCompanyActiveStatus) {
fields["active"] = {};
}
return fields;
}
@override
Map<String, String> defaultFilters() {
return { return {
"manufacturer_detail": "true", "manufacturer_detail": "true",
"supplier_detail": "true", "supplier_detail": "true",
"part_detail": "true", "manufacturer_part_detail": "true",
}; };
} }
int get manufacturerId => getInt("pk", subKey: "manufacturer_detail"); @override
Map<String, String> defaultListFilters() {
String get manufacturerName => return _filters();
getString("name", subKey: "manufacturer_detail");
String get MPN => getString("MPN", subKey: "manufacturer_part_detail");
String get manufacturerImage =>
(jsondata["manufacturer_detail"]?["image"] ??
jsondata["manufacturer_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
int get manufacturerPartId => getInt("manufacturer_part");
int get supplierId => getInt("supplier");
String get supplierName => getString("name", subKey: "supplier_detail");
String get supplierImage =>
(jsondata["supplier_detail"]?["image"] ??
jsondata["supplier_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
String get SKU => getString("SKU");
bool get active => getBool("active", backup: true);
int get partId => getInt("part");
double get inStock => getDouble("in_stock");
double get onOrder => getDouble("on_order");
String get partImage =>
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
as String;
String get partName => getString("name", subKey: "part_detail");
Map<String, dynamic> get partDetail => getMap("part_detail");
String get partDescription => getString("description", subKey: "part_detail");
String get note => getString("note");
String get packaging => getString("packaging");
String get pack_quantity {
if (InvenTreeAPI().apiVersion < 117) {
return getString("pack_size");
} else {
return getString("pack_quantity");
}
} }
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => Map<String, String> defaultGetFilters() {
InvenTreeSupplierPart.fromJson(json); return _filters();
}
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
String get manufacturerName => (jsondata["manufacturer_detail"]["name"] ?? "") as String;
String get manufacturerImage => (jsondata["manufacturer_detail"]["image"] ?? jsondata["manufacturer_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
int get manufacturerPartId => (jsondata["manufacturer_part"] ?? -1) as int;
int get supplierId => (jsondata["supplier"] ?? -1) as int;
String get supplierName => (jsondata["supplier_detail"]["name"] ?? "") as String;
String get supplierImage => (jsondata["supplier_detail"]["image"] ?? jsondata["supplier_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get SKU => (jsondata["SKU"] ?? "") as String;
String get MPN => (jsondata["MPN"] ?? "") as String;
int get partId => (jsondata["part"] ?? -1) as int;
String get partImage => (jsondata["part_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get partName => (jsondata["part_detail"]["full_name"] ?? "") as String;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
var part = InvenTreeSupplierPart.fromJson(json);
return part;
}
} }
class InvenTreeManufacturerPart extends InvenTreeModel { class InvenTreeManufacturerPart extends InvenTreeModel {
InvenTreeManufacturerPart() : super(); InvenTreeManufacturerPart() : super();
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String URL = "company/part/manufacturer/"; String url = "company/part/manufacturer/";
static const String MODEL_TYPE = "manufacturerpart";
@override @override
List<String> get rolesRequired => ["part"]; Map<String, String> defaultListFilters() {
return {
@override "manufacturer_detail": "true",
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"manufacturer": {},
"MPN": {},
"link": {},
}; };
return fields;
} }
int get partId => (jsondata["part"] ?? -1) as int;
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
String get MPN => (jsondata["MPN"] ?? "") as String;
@override @override
Map<String, String> defaultFilters() { InvenTreeModel createFromJson(Map<String, dynamic> json) {
return {"manufacturer_detail": "true", "part_detail": "true"}; var part = InvenTreeManufacturerPart.fromJson(json);
return part;
} }
int get partId => getInt("part");
String get partName => getString("name", subKey: "part_detail");
String get partDescription => getString("description", subKey: "part_detail");
String get partIPN => getString("IPN", subKey: "part_detail");
String get partImage =>
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
as String;
int get manufacturerId => getInt("manufacturer");
String get manufacturerName =>
getString("name", subKey: "manufacturer_detail");
String get manufacturerDescription =>
getString("description", subKey: "manufacturer_detail");
String get manufacturerImage =>
(jsondata["manufacturer_detail"]?["image"] ??
jsondata["manufacturer_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
String get MPN => getString("MPN");
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeManufacturerPart.fromJson(json);
} }

File diff suppressed because it is too large Load diff

View file

@ -1,48 +0,0 @@
import "package:inventree/inventree/model.dart";
/*
* Class representing a "notification"
*/
class InvenTreeNotification extends InvenTreeModel {
InvenTreeNotification() : super();
InvenTreeNotification.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeNotification createFromJson(Map<String, dynamic> json) {
return InvenTreeNotification.fromJson(json);
}
@override
String get URL => "notifications/";
@override
Map<String, String> defaultListFilters() {
// By default, only return 'unread' notifications
return {"read": "false"};
}
String get message => getString("message");
DateTime? get creationDate {
if (jsondata.containsKey("creation")) {
return DateTime.tryParse((jsondata["creation"] ?? "") as String);
} else {
return null;
}
}
/*
* Dismiss this notification (mark as read)
*/
Future<void> dismiss() async {
if (api.apiVersion >= 82) {
// "Modern" API endpoint operates a little differently
await update(values: {"read": "true"});
} else {
await api.post("${url}read/");
}
}
}

View file

@ -1,161 +0,0 @@
/*
* Base model for various "orders" which share common properties
*/
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
/*
* Generic class representing an "order"
*/
class InvenTreeOrder extends InvenTreeModel {
InvenTreeOrder() : super();
InvenTreeOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
String get issueDate => getString("issue_date");
String get startDate => getString("start_date");
String get completionDate => getDateString("complete_date");
String get creationDate => getDateString("creation_date");
String get shipmentDate => getDateString("shipment_date");
String get targetDate => getDateString("target_date");
int get lineItemCount => getInt("line_items", backup: 0);
int get completedLineItemCount => getInt("completed_lines", backup: 0);
int get shipmentCount => getInt("shipments_count", backup: 0);
int get completedShipmentCount =>
getInt("completed_shipments_count", backup: 0);
bool get complete => completedLineItemCount >= lineItemCount;
bool get overdue => getBool("overdue");
String get reference => getString("reference");
int get responsibleId => getInt("responsible");
String get responsibleName => getString("name", subKey: "responsible_detail");
String get responsibleLabel =>
getString("label", subKey: "responsible_detail");
// Project code information
int get projectCodeId => getInt("project_code");
String get projectCode => getString("code", subKey: "project_code_detail");
String get projectCodeDescription =>
getString("description", subKey: "project_code_detail");
bool get hasProjectCode => projectCode.isNotEmpty;
double? get totalPrice {
String price = getString("total_price");
if (price.isEmpty) {
return null;
} else {
return double.tryParse(price);
}
}
// Return the currency for this order
// Note that the nomenclature in the API changed at some point
String get totalPriceCurrency {
if (jsondata.containsKey("order_currency")) {
return getString("order_currency");
} else if (jsondata.containsKey("total_price_currency")) {
return getString("total_price_currency");
} else {
return "";
}
}
}
/*
* Generic class representing an "order line"
*/
class InvenTreeOrderLine extends InvenTreeModel {
InvenTreeOrderLine() : super();
InvenTreeOrderLine.fromJson(Map<String, dynamic> json) : super.fromJson(json);
bool get overdue => getBool("overdue");
double get quantity => getDouble("quantity");
String get reference => getString("reference");
int get orderId => getInt("order");
InvenTreePart? get part {
dynamic part_detail = jsondata["part_detail"];
if (part_detail == null) {
return null;
} else {
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
}
}
int get partId => getInt("pk", subKey: "part_detail");
String get partName => getString("name", subKey: "part_detail");
String get partImage {
String img = getString("thumbnail", subKey: "part_detail");
if (img.isEmpty) {
img = getString("image", subKey: "part_detail");
}
return img;
}
String get targetDate => getDateString("target_date");
}
/*
* Generic class representing an "ExtraLineItem"
*/
class InvenTreeExtraLineItem extends InvenTreeModel {
InvenTreeExtraLineItem() : super();
InvenTreeExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
int get orderId => getInt("order");
double get quantity => getDouble("quantity");
String get reference => getString("reference");
double get price => getDouble("price");
String get priceCurrency => getString("price_currency");
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"order": {
// The order cannot be edited
"hidden": true,
},
"reference": {},
"description": {},
"quantity": {},
"price": {},
"price_currency": {},
"link": {},
"notes": {},
};
}
}

View file

@ -1,77 +0,0 @@
import "package:inventree/inventree/model.dart";
class InvenTreeParameter extends InvenTreeModel {
InvenTreeParameter() : super();
InvenTreeParameter.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeParameter createFromJson(Map<String, dynamic> json) =>
InvenTreeParameter.fromJson(json);
@override
String get URL => "parameter/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"header": {
"type": "string",
"read_only": true,
"label": name,
"help_text": description,
"value": "",
},
"data": {"type": "string"},
"note": {},
};
return fields;
}
@override
String get name => getString("name", subKey: "template_detail");
@override
String get description => getString("description", subKey: "template_detail");
String get value => getString("data");
String get valueString {
String v = value;
if (units.isNotEmpty) {
v += " ";
v += units;
}
return v;
}
bool get as_bool => value.toLowerCase() == "true";
String get units => getString("units", subKey: "template_detail");
bool get is_checkbox =>
getBool("checkbox", subKey: "template_detail", backup: false);
// The model type of the instance this attachment is associated with
String get modelType => getString("model_type");
// The ID of the instance this attachment is associated with
int get modelId => getInt("model_id");
// Return a count of how many parameters exist against the specified model ID
Future<int> countParameters(String modelType, int modelId) async {
Map<String, String> filters = {};
if (!api.supportsModernParameters) {
return 0;
}
filters["model_type"] = modelType;
filters["model_id"] = modelId.toString();
return count(filters: filters);
}
}

View file

@ -1,61 +1,47 @@
import "dart:io"; import "dart:io";
import "dart:math";
import "package:flutter/material.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart"; import "package:flutter/material.dart";
import "package:inventree/widget/part/category_display.dart"; import "package:inventree/l10.dart";
import "package:inventree/widget/part/part_detail.dart";
import "package:inventree/inventree/model.dart";
/*
* Class representing the PartCategory database model
*/
class InvenTreePartCategory extends InvenTreeModel { class InvenTreePartCategory extends InvenTreeModel {
InvenTreePartCategory() : super(); InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String get URL => "part/category/"; String get URL => "part/category/";
static const String MODEL_TYPE = "partcategory";
@override @override
List<String> get rolesRequired => ["part"]; Map<String, dynamic> formFields() {
// Navigate to a detail page for this item return {
@override
Future<Object?> goToDetailPage(BuildContext context) async {
// Default implementation does not do anything...
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"name": {}, "name": {},
"description": {}, "description": {},
"parent": {}, "parent": {}
"structural": {},
}; };
return fields;
} }
String get pathstring => getString("pathstring"); @override
Map<String, String> defaultListFilters() {
String get parentPathString { return {
"active": "true",
"cascade": "false"
};
}
String get pathstring => (jsondata["pathstring"] ?? "") as String;
String get parentpathstring {
// TODO - Drive the refactor tractor through this
List<String> psplit = pathstring.split("/"); List<String> psplit = pathstring.split("/");
if (psplit.isNotEmpty) { if (psplit.isNotEmpty) {
@ -71,45 +57,47 @@ class InvenTreePartCategory extends InvenTreeModel {
return p; return p;
} }
// Return the number of parts in this category int get partcount => (jsondata["parts"] ?? 0) as int;
// Note that the API changed from 'parts' to 'part_count' (v69)
int get partcount =>
(jsondata["part_count"] ?? jsondata["parts"] ?? 0) as int;
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreePartCategory.fromJson(json); var cat = InvenTreePartCategory.fromJson(json);
// TODO ?
return cat;
}
} }
/*
* Class representing the PartTestTemplate database model
*/
class InvenTreePartTestTemplate extends InvenTreeModel { class InvenTreePartTestTemplate extends InvenTreeModel {
InvenTreePartTestTemplate() : super(); InvenTreePartTestTemplate() : super();
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String get URL => "part/test-template/"; String get URL => "part/test-template/";
static const String MODEL_TYPE = "parttesttemplate"; String get key => (jsondata["key"] ?? "") as String;
String get key => getString("key"); String get testName => (jsondata["test_name"] ?? "") as String;
String get testName => getString("test_name"); bool get required => (jsondata["required"] ?? false) as bool;
bool get required => getBool("required"); bool get requiresValue => (jsondata["requires_value"] ?? false) as bool;
bool get requiresValue => getBool("requires_value"); bool get requiresAttachment => (jsondata["requires_attachment"] ?? false) as bool;
bool get requiresAttachment => getBool("requires_attachment");
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreePartTestTemplate.fromJson(json); var template = InvenTreePartTestTemplate.fromJson(json);
return template;
}
bool passFailStatus() { bool passFailStatus() {
var result = latestResult(); var result = latestResult();
if (result == null) { if (result == null) {
@ -130,12 +118,12 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
return results.last; return results.last;
} }
} }
/*
* Class representing the Part database model
*/
class InvenTreePart extends InvenTreeModel { class InvenTreePart extends InvenTreeModel {
InvenTreePart() : super(); InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json); InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@ -143,23 +131,8 @@ class InvenTreePart extends InvenTreeModel {
@override @override
String get URL => "part/"; String get URL => "part/";
static const String MODEL_TYPE = "part";
@override @override
List<String> get rolesRequired => ["part"]; Map<String, dynamic> formFields() {
// Navigate to a detail page for this item
@override
Future<Object?> goToDetailPage(BuildContext context) async {
// Default implementation does not do anything...
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => PartDetailWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
return { return {
"name": {}, "name": {},
"description": {}, "description": {},
@ -187,8 +160,18 @@ class InvenTreePart extends InvenTreeModel {
} }
@override @override
Map<String, String> defaultFilters() { Map<String, String> defaultListFilters() {
return {"category_detail": "true"}; return {
"cascade": "false",
"active": "true",
};
}
@override
Map<String, String> defaultGetFilters() {
return {
"category_detail": "true", // Include category detail information
};
} }
// Cached list of stock items // Cached list of stock items
@ -197,50 +180,34 @@ class InvenTreePart extends InvenTreeModel {
int get stockItemCount => stockItems.length; int get stockItemCount => stockItems.length;
// Request stock items for this part // Request stock items for this part
Future<void> getStockItems( Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
BuildContext context, {
bool showDialog = false,
}) async {
await InvenTreeStockItem()
.list(filters: {"part": "${pk}", "in_stock": "true"})
.then((var items) {
stockItems.clear();
for (var item in items) { await InvenTreeStockItem().list(
if (item is InvenTreeStockItem) { filters: {
stockItems.add(item); "part": "${pk}",
} "in_stock": "true",
} },
}); ).then((var items) {
} stockItems.clear();
// Request pricing data for this part for (var item in items) {
Future<InvenTreePartPricing?> getPricing() async { if (item is InvenTreeStockItem) {
try { stockItems.add(item);
final response = await InvenTreeAPI().get("/api/part/${pk}/pricing/");
if (response.isValid()) {
final pricingData = response.data;
if (pricingData is Map<String, dynamic>) {
return InvenTreePartPricing.fromJson(pricingData);
} }
} }
} catch (e, stackTrace) { });
print("Exception while fetching pricing data for part $pk: $e");
sentryReportError("getPricing", e, stackTrace);
}
return null;
} }
int get supplierCount => getInt("suppliers", backup: 0); int get supplierCount => (jsondata["suppliers"] ?? 0) as int;
// Request supplier parts for this part // Request supplier parts for this part
Future<List<InvenTreeSupplierPart>> getSupplierParts() async { Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
List<InvenTreeSupplierPart> _supplierParts = []; List<InvenTreeSupplierPart> _supplierParts = [];
final parts = await InvenTreeSupplierPart().list( final parts = await InvenTreeSupplierPart().list(
filters: {"part": "${pk}"}, filters: {
"part": "${pk}",
}
); );
for (var result in parts) { for (var result in parts) {
@ -252,6 +219,7 @@ class InvenTreePart extends InvenTreeModel {
return _supplierParts; return _supplierParts;
} }
// Cached list of test templates // Cached list of test templates
List<InvenTreePartTestTemplate> testingTemplates = []; List<InvenTreePartTestTemplate> testingTemplates = [];
@ -259,9 +227,13 @@ class InvenTreePart extends InvenTreeModel {
// Request test templates from the serve // Request test templates from the serve
Future<void> getTestTemplates() async { Future<void> getTestTemplates() async {
InvenTreePartTestTemplate().list(filters: {"part": "${pk}"}).then((
var templates, InvenTreePartTestTemplate().list(
) { filters: {
"part": "${pk}",
},
).then((var templates) {
testingTemplates.clear(); testingTemplates.clear();
for (var t in templates) { for (var t in templates) {
@ -274,214 +246,196 @@ class InvenTreePart extends InvenTreeModel {
int? get defaultLocation => jsondata["default_location"] as int?; int? get defaultLocation => jsondata["default_location"] as int?;
double get onOrder => getDouble("ordering"); // Get the number of stock on order for this Part
double get onOrder => double.tryParse(jsondata["ordering"].toString()) ?? 0;
String get onOrderString => simpleNumberString(onOrder); String get onOrderString {
double get inStock { return simpleNumberString(onOrder);
if (jsondata.containsKey("total_in_stock")) {
return getDouble("total_in_stock");
} else {
return getDouble("in_stock");
}
}
String get inStockString => simpleNumberString(inStock);
// Get the 'available stock' for this Part
double get unallocatedStock {
double unallocated = 0;
// Note that the 'available_stock' was not added until API v35
if (jsondata.containsKey("unallocated_stock")) {
unallocated =
double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
} else {
unallocated = inStock;
} }
return max(0, unallocated); // Get the stock count for this Part
} double get inStock => double.tryParse(jsondata["in_stock"].toString()) ?? 0;
String get unallocatedStockString => simpleNumberString(unallocatedStock); String get inStockString {
String stockString({bool includeUnits = true}) { String q = simpleNumberString(inStock);
String q = unallocatedStockString;
if (unallocatedStock != inStock) { if (units.isNotEmpty) {
q += " / ${inStockString}"; q += " ${units}";
}
return q;
} }
if (includeUnits && units.isNotEmpty) { // Get the 'available stock' for this Part
q += " ${units}"; double get unallocatedStock {
// Note that the 'available_stock' was not added until API v35
if (jsondata.containsKey("unallocated_stock")) {
return double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
} else {
return inStock;
}
} }
return q; String get unallocatedStockString {
} String q = simpleNumberString(unallocatedStock);
String get units => getString("units"); if (units.isNotEmpty) {
q += " ${units}";
}
// Get the ID of the Part that this part is a variant of (or null) return q;
int? get variantOf => jsondata["variant_of"] as int?; }
// Get the number of units being build for this Part String stockString({bool includeUnits = true}) {
double get building => getDouble("building"); String q = unallocatedStockString;
// Get the number of BOMs this Part is used in (if it is a component) if (unallocatedStock != inStock) {
int get usedInCount => q += " / ${inStockString}";
jsondata.containsKey("used_in") ? getInt("used_in", backup: 0) : 0; }
bool get isAssembly => getBool("assembly"); if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
bool get isComponent => getBool("component"); return q;
}
bool get isPurchaseable => getBool("purchaseable"); String get units => (jsondata["units"] ?? "") as String;
bool get isSalable => getBool("salable"); // Get the ID of the Part that this part is a variant of (or null)
int? get variantOf => jsondata["variant_of"] as int?;
bool get isActive => getBool("active"); // Get the number of units being build for this Part
double get building => double.tryParse(jsondata["building"].toString()) ?? 0;
bool get isVirtual => getBool("virtual"); // Get the number of BOM items in this Part (if it is an assembly)
int get bomItemCount => (jsondata["bom_items"] ?? 0) as int;
bool get isTemplate => getBool("is_template"); // Get the number of BOMs this Part is used in (if it is a component)
int get usedInCount => (jsondata["used_in"] ?? 0) as int;
bool get isTrackable => getBool("trackable"); bool get isAssembly => (jsondata["assembly"] ?? false) as bool;
bool get isTestable => getBool("testable"); bool get isComponent => (jsondata["component"] ?? false) as bool;
// Get the IPN (internal part number) for the Part instance bool get isPurchaseable => (jsondata["purchaseable"] ?? false) as bool;
String get IPN => getString("IPN");
// Get the revision string for the Part instance bool get isSalable => (jsondata["salable"] ?? false) as bool;
String get revision => getString("revision");
// Get the category ID for the Part instance (or "null" if does not exist) bool get isActive => (jsondata["active"] ?? false) as bool;
int get categoryId => getInt("category");
// Get the category name for the Part instance bool get isVirtual => (jsondata["virtual"] ?? false) as bool;
String get categoryName {
// Inavlid category ID
if (categoryId <= 0) return "";
if (!jsondata.containsKey("category_detail")) return ""; bool get isTrackable => (jsondata["trackable"] ?? false) as bool;
return (jsondata["category_detail"]?["name"] ?? "") as String; // Get the IPN (internal part number) for the Part instance
} String get IPN => (jsondata["IPN"] ?? "") as String;
// Get the category description for the Part instance // Get the revision string for the Part instance
String get categoryDescription { String get revision => (jsondata["revision"] ?? "") as String;
// Invalid category ID
if (categoryId <= 0) return "";
if (!jsondata.containsKey("category_detail")) return ""; // Get the category ID for the Part instance (or "null" if does not exist)
int get categoryId => (jsondata["category"] ?? -1) as int;
return (jsondata["category_detail"]?["description"] ?? "") as String; // Get the category name for the Part instance
} String get categoryName {
// Inavlid category ID
if (categoryId <= 0) return "";
// Get the image URL for the Part instance if (!jsondata.containsKey("category_detail")) return "";
String get _image => getString("image");
// Get the thumbnail URL for the Part instance return (jsondata["category_detail"]?["name"] ?? "") as String;
String get _thumbnail => getString("thumbnail"); }
// Return the fully-qualified name for the Part instance // Get the category description for the Part instance
String get fullname { String get categoryDescription {
String fn = getString("full_name"); // Invalid category ID
if (categoryId <= 0) return "";
if (fn.isNotEmpty) return fn; if (!jsondata.containsKey("category_detail")) return "";
List<String> elements = []; return (jsondata["category_detail"]?["description"] ?? "") as String;
}
// Get the image URL for the Part instance
String get _image => (jsondata["image"] ?? "") as String;
if (IPN.isNotEmpty) elements.add(IPN); // Get the thumbnail URL for the Part instance
String get _thumbnail => (jsondata["thumbnail"] ?? "") as String;
elements.add(name); // Return the fully-qualified name for the Part instance
String get fullname {
if (revision.isNotEmpty) elements.add(revision); String fn = (jsondata["full_name"] ?? "") as String;
return elements.join(" | "); if (fn.isNotEmpty) return fn;
}
// Return a path to the image for this Part List<String> elements = [];
String get image {
// Use thumbnail as a backup
String img = _image.isNotEmpty ? _image : _thumbnail;
return img.isNotEmpty ? img : InvenTreeAPI.staticImage; if (IPN.isNotEmpty) elements.add(IPN);
}
// Return a path to the thumbnail for this part elements.add(name);
String get thumbnail {
// Use image as a backup
String img = _thumbnail.isNotEmpty ? _thumbnail : _image;
return img.isNotEmpty ? img : InvenTreeAPI.staticThumb; if (revision.isNotEmpty) elements.add(revision);
}
Future<bool> uploadImage(File image) async { return elements.join(" | ");
// Upload file against this part }
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
image,
method: "PATCH",
name: "image",
);
return response.successful(); // Return a path to the image for this Part
} String get image {
// Use thumbnail as a backup
String img = _image.isNotEmpty ? _image : _thumbnail;
// Return the "starred" status of this part return img.isNotEmpty ? img : InvenTreeAPI.staticImage;
bool get starred => getBool("starred"); }
// Return a path to the thumbnail for this part
String get thumbnail {
// Use image as a backup
String img = _thumbnail.isNotEmpty ? _thumbnail : _image;
return img.isNotEmpty ? img : InvenTreeAPI.staticThumb;
}
Future<bool> uploadImage(File image) async {
// Upload file against this part
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
image,
method: "PATCH",
name: "image",
);
return response.successful();
}
// Return the "starred" status of this part
bool get starred => (jsondata["starred"] ?? false) as bool;
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreePart.fromJson(json);
var part = InvenTreePart.fromJson(json);
return part;
}
} }
class InvenTreePartPricing extends InvenTreeModel {
InvenTreePartPricing() : super();
InvenTreePartPricing.fromJson(Map<String, dynamic> json) class InvenTreePartAttachment extends InvenTreeAttachment {
: super.fromJson(json);
InvenTreePartAttachment() : super();
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
List<String> get rolesRequired => ["part"]; String get URL => "part/attachment/";
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreePartPricing.fromJson(json); return InvenTreePartAttachment.fromJson(json);
}
// Price data accessors }
String get currency => getString("currency", backup: "USD");
double? get overallMin => getDoubleOrNull("overall_min");
double? get overallMax => getDoubleOrNull("overall_max");
double? get overrideMin => getDoubleOrNull("override_min");
double? get overrideMax => getDoubleOrNull("override_max");
String get overrideMinCurrency =>
getString("override_min_currency", backup: currency);
String get overrideMaxCurrency =>
getString("override_max_currency", backup: currency);
double? get bomCostMin => getDoubleOrNull("bom_cost_min");
double? get bomCostMax => getDoubleOrNull("bom_cost_max");
double? get purchaseCostMin => getDoubleOrNull("purchase_cost_min");
double? get purchaseCostMax => getDoubleOrNull("purchase_cost_max");
double? get internalCostMin => getDoubleOrNull("internal_cost_min");
double? get internalCostMax => getDoubleOrNull("internal_cost_max");
double? get supplierPriceMin => getDoubleOrNull("supplier_price_min");
double? get supplierPriceMax => getDoubleOrNull("supplier_price_max");
double? get variantCostMin => getDoubleOrNull("variant_cost_min");
double? get variantCostMax => getDoubleOrNull("variant_cost_max");
double? get salePriceMin => getDoubleOrNull("sale_price_min");
double? get salePriceMax => getDoubleOrNull("sale_price_max");
double? get saleHistoryMin => getDoubleOrNull("sale_history_min");
double? get saleHistoryMax => getDoubleOrNull("sale_history_max");
}

View file

@ -1,27 +0,0 @@
import "package:inventree/inventree/model.dart";
/*
* Class representing the ProjectCode database model
*/
class InvenTreeProjectCode extends InvenTreeModel {
InvenTreeProjectCode() : super();
InvenTreeProjectCode.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeProjectCode.fromJson(json);
@override
String get URL => "project-code/";
static const String MODEL_TYPE = "projectcode";
@override
Map<String, Map<String, dynamic>> formFields() {
return {"code": {}, "description": {}};
}
String get code => getString("code");
}

View file

@ -1,91 +1,73 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/orders.dart";
import "package:inventree/widget/order/extra_line_detail.dart";
import "package:inventree/widget/order/purchase_order_detail.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/api_form.dart"; // TODO: In the future, status codes should be retrieved from the server
import "package:inventree/l10.dart"; const int PO_STATUS_PENDING = 10;
const int PO_STATUS_PLACED = 20;
const int PO_STATUS_COMPLETE = 30;
const int PO_STATUS_CANCELLED = 40;
const int PO_STATUS_LOST = 50;
const int PO_STATUS_RETURNED = 60;
class InvenTreePurchaseOrder extends InvenTreeModel {
/*
* Class representing an individual PurchaseOrder instance
*/
class InvenTreePurchaseOrder extends InvenTreeOrder {
InvenTreePurchaseOrder() : super(); InvenTreePurchaseOrder() : super();
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePurchaseOrder.fromJson(json);
@override @override
String get URL => "order/po/"; String get URL => "order/po/";
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => PurchaseOrderDetailWidget(this)),
);
}
static const String MODEL_TYPE = "purchaseorder";
@override
List<String> get rolesRequired => ["purchase_order"];
String get receive_url => "${url}receive/"; String get receive_url => "${url}receive/";
@override @override
Map<String, Map<String, dynamic>> formFields() { Map<String, dynamic> formFields() {
Map<String, Map<String, dynamic>> fields = { return {
"reference": {}, "reference": {},
"supplier": {
"filters": {"is_supplier": true},
},
"supplier_reference": {}, "supplier_reference": {},
"description": {}, "description": {},
"project_code": {},
"destination": {},
"start_date": {},
"target_date": {}, "target_date": {},
"link": {}, "link": {},
"responsible": {}, "responsible": {},
"contact": {
"filters": {"company": supplierId},
},
}; };
if (!InvenTreeAPI().supportsProjectCodes) {
fields.remove("project_code");
}
if (!InvenTreeAPI().supportsPurchaseOrderDestination) {
fields.remove("destination");
}
if (!InvenTreeAPI().supportsStartDate) {
fields.remove("start_date");
}
return fields;
} }
@override @override
Map<String, String> defaultFilters() { Map<String, String> defaultGetFilters() {
return {"supplier_detail": "true"}; return {
"supplier_detail": "true",
};
} }
int get supplierId => getInt("supplier"); @override
Map<String, String> defaultListFilters() {
return {
"supplier_detail": "true",
};
}
String get issueDate => (jsondata["issue_date"] ?? "") as String;
String get completeDate => (jsondata["complete_date"] ?? "") as String;
String get creationDate => (jsondata["creation_date"] ?? "") as String;
String get targetDate => (jsondata["target_date"] ?? "") as String;
int get lineItemCount => (jsondata["line_items"] ?? 0) as int;
bool get overdue => (jsondata["overdue"] ?? false) as bool;
String get reference => (jsondata["reference"] ?? "") as String;
int get responsibleId => (jsondata["responsible"] ?? -1) as int;
int get supplierId => (jsondata["supplier"] ?? -1) as int;
InvenTreeCompany? get supplier { InvenTreeCompany? get supplier {
dynamic supplier_detail = jsondata["supplier_detail"]; dynamic supplier_detail = jsondata["supplier_detail"];
if (supplier_detail == null) { if (supplier_detail == null) {
@ -95,30 +77,24 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
} }
} }
String get supplierReference => getString("supplier_reference"); String get supplierReference => (jsondata["supplier_reference"] ?? "") as String;
int get destinationId => getInt("destination"); int get status => (jsondata["status"] ?? -1) as int;
bool get isOpen => api.PurchaseOrderStatus.isNameIn(status, [ String get statusText => (jsondata["status_text"] ?? "") as String;
"PENDING",
"PLACED",
"ON_HOLD",
]);
bool get isPending => bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED;
api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isPlaced => api.PurchaseOrderStatus.isNameIn(status, ["PLACED"]); bool get isPlaced => status == PO_STATUS_PLACED;
bool get isFailed => api.PurchaseOrderStatus.isNameIn(status, [ bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED;
"CANCELLED",
"LOST",
"RETURNED",
]);
Future<List<InvenTreePOLineItem>> getLineItems() async { Future<List<InvenTreePOLineItem>> getLineItems() async {
final results = await InvenTreePOLineItem().list( final results = await InvenTreePOLineItem().list(
filters: {"order": "${pk}"}, filters: {
"order": "${pk}",
}
); );
List<InvenTreePOLineItem> items = []; List<InvenTreePOLineItem> items = [];
@ -132,92 +108,77 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
return items; return items;
} }
/// Mark this order as "placed" / "issued" @override
Future<void> issueOrder() async { InvenTreeModel createFromJson(Map<String, dynamic> json) {
// Order can only be placed when the order is 'pending' return InvenTreePurchaseOrder.fromJson(json);
if (!isPending) {
return;
}
showLoadingOverlay();
await api.post("${url}issue/", expectedStatusCode: 201);
hideLoadingOverlay();
}
/// Mark this order as "cancelled"
Future<void> cancelOrder() async {
if (!isOpen) {
return;
}
showLoadingOverlay();
await api.post("${url}cancel/", expectedStatusCode: 201);
hideLoadingOverlay();
} }
} }
class InvenTreePOLineItem extends InvenTreeOrderLine { class InvenTreePOLineItem extends InvenTreeModel {
InvenTreePOLineItem() : super(); InvenTreePOLineItem() : super();
InvenTreePOLineItem.fromJson(Map<String, dynamic> json) InvenTreePOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePOLineItem.fromJson(json);
@override @override
String get URL => "order/po-line/"; String get URL => "order/po-line/";
@override @override
List<String> get rolesRequired => ["purchase_order"]; Map<String, dynamic> formFields() {
@override
Map<String, Map<String, dynamic>> formFields() {
return { return {
"part": { // TODO: @Guusggg Not sure what will come here.
// We cannot edit the supplier part field here // "quantity": {},
"hidden": true, // "reference": {},
}, // "notes": {},
"order": { // "order": {},
// We cannot edit the order field here // "part": {},
"hidden": true, "received": {},
}, // "purchase_price": {},
"reference": {}, // "purchase_price_currency": {},
"quantity": {}, // "destination": {}
"purchase_price": {},
"purchase_price_currency": {},
"destination": {},
"notes": {},
"link": {},
}; };
} }
@override @override
Map<String, String> defaultFilters() { Map<String, String> defaultGetFilters() {
return {"part_detail": "true", "order_detail": "true"}; return {
"part_detail": "true",
};
} }
double get received => getDouble("received"); @override
Map<String, String> defaultListFilters() {
return {
"part_detail": "true",
};
}
bool get isComplete => received >= quantity; bool get isComplete => received >= quantity;
double get progressRatio { double get quantity => (jsondata["quantity"] ?? 0) as double;
if (quantity <= 0 || received <= 0) {
return 0;
}
return received / quantity; double get received => (jsondata["received"] ?? 0) as double;
}
String get progressString =>
simpleNumberString(received) + " / " + simpleNumberString(quantity);
double get outstanding => quantity - received; double get outstanding => quantity - received;
int get supplierPartId => getInt("part"); String get reference => (jsondata["reference"] ?? "") as String;
int get orderId => (jsondata["order"] ?? -1) as int;
int get supplierPartId => (jsondata["part"] ?? -1) as int;
InvenTreePart? get part {
dynamic part_detail = jsondata["part_detail"];
if (part_detail == null) {
return null;
} else {
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
}
}
InvenTreeSupplierPart? get supplierPart { InvenTreeSupplierPart? get supplierPart {
dynamic detail = jsondata["supplier_part_detail"]; dynamic detail = jsondata["supplier_part_detail"];
if (detail == null) { if (detail == null) {
@ -227,112 +188,18 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
} }
} }
InvenTreePurchaseOrder? get purchaseOrder { double get purchasePrice => double.parse((jsondata["purchase_price"] ?? "") as String);
dynamic detail = jsondata["order_detail"];
if (detail == null) { String get purchasePriceCurrency => (jsondata["purchase_price_currency"] ?? "") as String;
return null;
} else {
return InvenTreePurchaseOrder.fromJson(detail as Map<String, dynamic>);
}
}
String get SKU => getString("SKU", subKey: "supplier_part_detail"); String get purchasePriceString => (jsondata["purchase_price_string"] ?? "") as String;
double get purchasePrice => getDouble("purchase_price"); int get destination => (jsondata["destination"] ?? -1) as int;
String get purchasePriceCurrency => getString("purchase_price_currency"); Map<String, dynamic> get destinationDetail => (jsondata["destination_detail"] ?? {}) as Map<String, dynamic>;
int get destinationId => getInt("destination"); @override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
Map<String, dynamic> get orderDetail => getMap("order_detail"); return InvenTreePOLineItem.fromJson(json);
Map<String, dynamic> get destinationDetail => getMap("destination_detail");
// Receive this line item into stock
Future<void> receive(
BuildContext context, {
int? destination,
double? quantity,
String? barcode,
Function? onSuccess,
}) async {
// Infer the destination location from the line item if not provided
if (destinationId > 0) {
destination = destinationId;
}
destination ??= (orderDetail["destination"]) as int?;
quantity ??= outstanding;
// Construct form fields
Map<String, dynamic> fields = {
"line_item": {
"parent": "items",
"nested": true,
"hidden": true,
"value": pk,
},
"quantity": {"parent": "items", "nested": true, "value": quantity},
"location": {},
"status": {"parent": "items", "nested": true},
"batch_code": {"parent": "items", "nested": true},
"barcode": {
"parent": "items",
"nested": true,
"type": "barcode",
"label": L10().barcodeAssign,
"value": barcode,
"required": false,
},
};
if (destination != null && destination > 0) {
fields["location"]?["value"] = destination;
}
InvenTreePurchaseOrder? order = purchaseOrder;
if (order != null) {
await launchApiForm(
context,
L10().receiveItem,
order.receive_url,
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) {
if (onSuccess != null) {
onSuccess();
}
},
);
}
}
}
class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
InvenTreePOExtraLineItem() : super();
InvenTreePOExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePOExtraLineItem.fromJson(json);
@override
String get URL => "order/po-extra-line/";
@override
List<String> get rolesRequired => ["purchase_order"];
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
);
} }
} }

View file

@ -1,430 +0,0 @@
import "package:flutter/material.dart";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/orders.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/order/so_shipment_detail.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/order/extra_line_detail.dart";
import "package:inventree/widget/order/sales_order_detail.dart";
/*
* Class representing an individual SalesOrder
*/
class InvenTreeSalesOrder extends InvenTreeOrder {
InvenTreeSalesOrder() : super();
InvenTreeSalesOrder.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrder.fromJson(json);
@override
String get URL => "order/so/";
static const String MODEL_TYPE = "salesorder";
@override
List<String> get rolesRequired => ["sales_order"];
String get allocate_url => "${url}allocate/";
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"reference": {},
"customer": {
"filters": {"is_customer": true, "active": true},
},
"customer_reference": {},
"description": {},
"project_code": {},
"start_date": {},
"target_date": {},
"link": {},
"responsible": {},
"contact": {
"filters": {"company": customerId},
},
};
if (!InvenTreeAPI().supportsProjectCodes) {
fields.remove("project_code");
}
if (!InvenTreeAPI().supportsContactModel) {
fields.remove("contact");
}
if (!InvenTreeAPI().supportsStartDate) {
fields.remove("start_date");
}
return fields;
}
@override
Map<String, String> defaultFilters() {
return {"customer_detail": "true"};
}
Future<void> issueOrder() async {
if (!isPending) {
return;
}
showLoadingOverlay();
await api.post("${url}issue/", expectedStatusCode: 201);
hideLoadingOverlay();
}
/// Mark this order as "cancelled"
Future<void> cancelOrder() async {
if (!isOpen) {
return;
}
showLoadingOverlay();
await api.post("${url}cancel/", expectedStatusCode: 201);
hideLoadingOverlay();
}
int get customerId => getInt("customer");
InvenTreeCompany? get customer {
dynamic customer_detail = jsondata["customer_detail"];
if (customer_detail == null) {
return null;
} else {
return InvenTreeCompany.fromJson(customer_detail as Map<String, dynamic>);
}
}
String get customerReference => getString("customer_reference");
bool get isOpen => api.SalesOrderStatus.isNameIn(status, [
"PENDING",
"IN_PROGRESS",
"ON_HOLD",
]);
bool get isPending =>
api.SalesOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isInProgress =>
api.SalesOrderStatus.isNameIn(status, ["IN_PROGRESS"]);
bool get isComplete => api.SalesOrderStatus.isNameIn(status, ["SHIPPED"]);
}
/*
* Class representing an individual line item in a SalesOrder
*/
class InvenTreeSOLineItem extends InvenTreeOrderLine {
InvenTreeSOLineItem() : super();
InvenTreeSOLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSOLineItem.fromJson(json);
@override
String get URL => "order/so-line/";
@override
List<String> get rolesRequired => ["sales_order"];
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"order": {"hidden": true},
"part": {
"filters": {"salable": true},
},
"quantity": {},
"reference": {},
"notes": {},
"link": {},
};
}
Map<String, Map<String, dynamic>> allocateFormFields() {
return {
"line_item": {"parent": "items", "nested": true, "hidden": true},
"stock_item": {"parent": "items", "nested": true, "filters": {}},
"quantity": {"parent": "items", "nested": true},
"shipment": {"filters": {}},
};
}
@override
Map<String, String> defaultFilters() {
return {"part_detail": "true"};
}
double get allocated => getDouble("allocated");
bool get isAllocated => allocated >= quantity;
double get allocatedRatio {
if (quantity <= 0 || allocated <= 0) {
return 0;
}
return allocated / quantity;
}
double get unallocatedQuantity {
double unallocated = quantity - allocated;
if (unallocated < 0) {
unallocated = 0;
}
return unallocated;
}
String get allocatedString =>
simpleNumberString(allocated) + " / " + simpleNumberString(quantity);
double get shipped => getDouble("shipped");
double get outstanding => quantity - shipped;
double get availableStock => getDouble("available_stock");
double get progressRatio {
if (quantity <= 0 || shipped <= 0) {
return 0;
}
return shipped / quantity;
}
String get progressString =>
simpleNumberString(shipped) + " / " + simpleNumberString(quantity);
bool get isComplete => shipped >= quantity;
double get available =>
getDouble("available_stock") + getDouble("available_variant_stock");
double get salePrice => getDouble("sale_price");
String get salePriceCurrency => getString("sale_price_currency");
}
class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
InvenTreeSOExtraLineItem() : super();
InvenTreeSOExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSOExtraLineItem.fromJson(json);
@override
String get URL => "order/so-extra-line/";
@override
List<String> get rolesRequired => ["sales_order"];
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
);
}
}
/*
* Class representing a sales order shipment
*/
class InvenTreeSalesOrderShipment extends InvenTreeModel {
InvenTreeSalesOrderShipment() : super();
InvenTreeSalesOrderShipment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrderShipment.fromJson(json);
@override
String get URL => "/order/so/shipment/";
String get SHIP_SHIPMENT_URL => "/order/so/shipment/${pk}/ship/";
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => SOShipmentDetailWidget(this)),
);
}
@override
List<String> get rolesRequired => ["sales_order"];
static const String MODEL_TYPE = "salesordershipment";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"order": {},
"reference": {},
"tracking_number": {},
"invoice_number": {},
"link": {},
};
return fields;
}
int get orderId => getInt("order");
InvenTreeSalesOrder? get order {
dynamic order_detail = jsondata["order_detail"];
if (order_detail == null) {
return null;
} else {
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
}
}
String get reference => getString("reference");
String get tracking_number => getString("tracking_number");
String get invoice_number => getString("invoice_number");
String? get shipment_date => getString("shipment_date");
String? get delivery_date => getString("delivery_date");
int? get checked_by_id => getInt("checked_by");
bool get isChecked => checked_by_id != null && checked_by_id! > 0;
bool get isShipped => shipment_date != null && shipment_date!.isNotEmpty;
bool get isDelivered => delivery_date != null && delivery_date!.isNotEmpty;
}
/*
* Class representing an allocation of stock against a SalesOrderShipment
*/
class InvenTreeSalesOrderAllocation extends InvenTreeModel {
InvenTreeSalesOrderAllocation() : super();
InvenTreeSalesOrderAllocation.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrderAllocation.fromJson(json);
@override
String get URL => "/order/so-allocation/";
@override
List<String> get rolesRequired => ["sales_order"];
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
"order_detail": "true",
"item_detail": "true",
"location_detail": "true",
};
}
static const String MODEL_TYPE = "salesorderallocation";
int get orderId => getInt("order");
InvenTreeSalesOrder? get order {
dynamic order_detail = jsondata["order_detail"];
if (order_detail == null) {
return null;
} else {
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
}
}
int get stockItemId => getInt("item");
InvenTreeStockItem? get stockItem {
dynamic item_detail = jsondata["item_detail"];
if (item_detail == null) {
return null;
} else {
return InvenTreeStockItem.fromJson(item_detail as Map<String, dynamic>);
}
}
int get partId => getInt("part");
InvenTreePart? get part {
dynamic part_detail = jsondata["part_detail"];
if (part_detail == null) {
return null;
} else {
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
}
}
int get shipmentId => getInt("shipment");
bool get hasShipment => shipmentId > 0;
InvenTreeSalesOrderShipment? get shipment {
dynamic shipment_detail = jsondata["shipment_detail"];
if (shipment_detail == null) {
return null;
} else {
return InvenTreeSalesOrderShipment.fromJson(
shipment_detail as Map<String, dynamic>,
);
}
}
int get locationId => getInt("location");
InvenTreeStockLocation? get location {
dynamic location_detail = jsondata["location_detail"];
if (location_detail == null) {
return null;
} else {
return InvenTreeStockLocation.fromJson(
location_detail as Map<String, dynamic>,
);
}
}
}

View file

@ -1,16 +1,14 @@
import "dart:io"; import "dart:io";
import "package:device_info_plus/device_info_plus.dart"; import "package:device_info_plus/device_info_plus.dart";
import "package:inventree/helpers.dart"; import "package:inventree/app_settings.dart";
import "package:one_context/one_context.dart";
import "package:package_info_plus/package_info_plus.dart"; import "package:package_info_plus/package_info_plus.dart";
import "package:sentry_flutter/sentry_flutter.dart"; import "package:sentry_flutter/sentry_flutter.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/dsn.dart";
import "package:inventree/preferences.dart";
Future<Map<String, dynamic>> getDeviceInfo() async { Future<Map<String, dynamic>> getDeviceInfo() async {
// Extract device information // Extract device information
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
@ -30,6 +28,7 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
"identifierForVendor": iosDeviceInfo.identifierForVendor, "identifierForVendor": iosDeviceInfo.identifierForVendor,
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice, "isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
}; };
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
final androidDeviceInfo = await deviceInfo.androidInfo; final androidDeviceInfo = await deviceInfo.androidInfo;
@ -38,13 +37,13 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
"model": androidDeviceInfo.model, "model": androidDeviceInfo.model,
"device": androidDeviceInfo.device, "device": androidDeviceInfo.device,
"id": androidDeviceInfo.id, "id": androidDeviceInfo.id,
"androidId": androidDeviceInfo.id, "androidId": androidDeviceInfo.androidId,
"brand": androidDeviceInfo.brand, "brand": androidDeviceInfo.brand,
"display": androidDeviceInfo.display, "display": androidDeviceInfo.display,
"hardware": androidDeviceInfo.hardware, "hardware": androidDeviceInfo.hardware,
"manufacturer": androidDeviceInfo.manufacturer, "manufacturer": androidDeviceInfo.manufacturer,
"product": androidDeviceInfo.product, "product": androidDeviceInfo.product,
"systemVersion": androidDeviceInfo.version.release, "version": androidDeviceInfo.version.release,
"supported32BitAbis": androidDeviceInfo.supported32BitAbis, "supported32BitAbis": androidDeviceInfo.supported32BitAbis,
"supported64BitAbis": androidDeviceInfo.supported64BitAbis, "supported64BitAbis": androidDeviceInfo.supported64BitAbis,
"supportedAbis": androidDeviceInfo.supportedAbis, "supportedAbis": androidDeviceInfo.supportedAbis,
@ -55,11 +54,12 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
return device_info; return device_info;
} }
Map<String, dynamic> getServerInfo() => { Map<String, dynamic> getServerInfo() => {
"version": InvenTreeAPI().serverVersion, "version": InvenTreeAPI().version,
"apiVersion": InvenTreeAPI().apiVersion,
}; };
Future<Map<String, dynamic>> getAppInfo() async { Future<Map<String, dynamic>> getAppInfo() async {
// Add app info // Add app info
final package_info = await PackageInfo.fromPlatform(); final package_info = await PackageInfo.fromPlatform();
@ -72,6 +72,7 @@ Future<Map<String, dynamic>> getAppInfo() async {
}; };
} }
bool isInDebugMode() { bool isInDebugMode() {
bool inDebugMode = false; bool inDebugMode = false;
@ -80,13 +81,7 @@ bool isInDebugMode() {
return inDebugMode; return inDebugMode;
} }
Future<bool> sentryReportMessage( Future<bool> sentryReportMessage(String message, {Map<String, String>? context}) async {
String message, {
Map<String, String>? context,
}) async {
if (SENTRY_DSN_KEY.isEmpty) {
return false;
}
final server_info = getServerInfo(); final server_info = getServerInfo();
final app_info = await getAppInfo(); final app_info = await getAppInfo();
@ -103,22 +98,23 @@ Future<bool> sentryReportMessage(
// We don't care about the server address, only the path and query parameters! // We don't care about the server address, only the path and query parameters!
// Overwrite the provided URL // Overwrite the provided URL
context["url"] = uri.path + "?" + uri.query; context["url"] = uri.path + "?" + uri.query;
} catch (error) { } catch (error) {
// Ignore if any errors are thrown here // Ignore if any errors are thrown here
} }
} }
} }
print("Sending user message to Sentry: ${message}, ${context}"); print("Sending user message to Sentry: ${message}, ${context}");
if (isInDebugMode()) { if (isInDebugMode()) {
print("----- In dev mode. Not sending message to Sentry.io -----"); print("----- In dev mode. Not sending message to Sentry.io -----");
return true; return true;
} }
final upload = final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
as bool;
if (!upload) { if (!upload) {
print("----- Error reporting disabled -----"); print("----- Error reporting disabled -----");
@ -126,16 +122,13 @@ Future<bool> sentryReportMessage(
} }
Sentry.configureScope((scope) { Sentry.configureScope((scope) {
scope.setContexts("server", server_info); scope.setExtra("server", server_info);
scope.setContexts("app", app_info); scope.setExtra("app", app_info);
scope.setContexts("device", device_info); scope.setExtra("device", device_info);
if (context != null) { if (context != null) {
scope.setContexts("context", context); scope.setExtra("context", context);
} }
// Catch stacktrace data if possible
scope.setContexts("stacktrace", StackTrace.current.toString());
}); });
try { try {
@ -148,19 +141,8 @@ Future<bool> sentryReportMessage(
} }
} }
/*
* Report an error message to sentry.io Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
*/
Future<void> sentryReportError(
String source,
dynamic error,
StackTrace? stackTrace, {
Map<String, String> context = const {},
}) async {
if (sentryIgnoreError(error)) {
// No action on this error
return;
}
print("----- Sentry Intercepted error: $error -----"); print("----- Sentry Intercepted error: $error -----");
print(stackTrace); print(stackTrace);
@ -169,85 +151,32 @@ Future<void> sentryReportError(
// check if you are running in dev mode using an assertion and omit sending // check if you are running in dev mode using an assertion and omit sending
// the report. // the report.
if (isInDebugMode()) { if (isInDebugMode()) {
print("----- In dev mode. Not sending report to Sentry.io -----"); print("----- In dev mode. Not sending report to Sentry.io -----");
return; return;
} }
if (SENTRY_DSN_KEY.isEmpty) { final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
return;
}
final upload =
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
as bool;
if (!upload) { if (!upload) {
print("----- Error reporting disabled -----"); print("----- Error reporting disabled -----");
return; return;
} }
// Some errors are outside our control, and we do not want to "pollute" the uploaded data
if (source == "FlutterError.onError") {
String errorString = error.toString();
// Missing media file
if (errorString.contains("HttpException") &&
errorString.contains("404") &&
errorString.contains("/media/")) {
return;
}
// Local file system exception
if (errorString.contains("FileSystemException")) {
return;
}
}
final server_info = getServerInfo(); final server_info = getServerInfo();
final app_info = await getAppInfo(); final app_info = await getAppInfo();
final device_info = await getDeviceInfo(); final device_info = await getDeviceInfo();
// Ensure we pass the 'source' of the error
context["source"] = source;
if (hasContext()) {
final ctx = OneContext().context;
if (ctx != null) {
context["widget"] = ctx.widget.toString();
context["widgetType"] = ctx.widget.runtimeType.toString();
}
}
Sentry.configureScope((scope) { Sentry.configureScope((scope) {
scope.setContexts("server", server_info); scope.setExtra("server", server_info);
scope.setContexts("app", app_info); scope.setExtra("app", app_info);
scope.setContexts("device", device_info); scope.setExtra("device", device_info);
scope.setContexts("context", context);
}); });
Sentry.captureException(error, stackTrace: stackTrace) Sentry.captureException(error, stackTrace: stackTrace).catchError((error) {
.catchError((error) { print("Error uploading information to Sentry.io:");
print("Error uploading information to Sentry.io:"); print(error);
print(error); }).then((response) {
return SentryId.empty(); print("Uploaded information to Sentry.io : ${response.toString()}");
}) });
.then((response) {
print("Uploaded information to Sentry.io : ${response.toString()}");
});
}
/*
* Test if a certain error should be ignored by Sentry
*/
bool sentryIgnoreError(dynamic error) {
// Ignore 404 errors for media files
if (error is HttpException) {
if (error.uri.toString().contains("/media/") &&
error.message.contains("404")) {
return true;
}
}
return false;
} }

View file

@ -1,145 +0,0 @@
/*
* Code for querying the server for various status code data,
* so that we do not have to duplicate those codes in the app.
*
* Ref: https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/status_codes.py
*/
import "package:flutter/material.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
/*
* Base class definition for a "status code" definition.
*/
class InvenTreeStatusCode {
InvenTreeStatusCode(this.URL);
final String URL;
// Internal status code data loaded from server
Map<String, dynamic> data = {};
/*
* Construct a list of "choices" suitable for a form
*/
List<dynamic> get choices {
List<dynamic> _choices = [];
for (String key in data.keys) {
dynamic _entry = data[key];
if (_entry is Map<String, dynamic>) {
_choices.add({"value": _entry["key"], "display_name": _entry["label"]});
}
}
return _choices;
}
// Load status code information from the server
Future<void> load({bool forceReload = false}) async {
// Return internally cached data
if (data.isNotEmpty && !forceReload) {
return;
}
// The server must support this feature!
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
return;
}
debug("Loading status codes from ${URL}");
APIResponse response = await InvenTreeAPI().get(URL);
if (response.statusCode == 200) {
Map<String, dynamic> results = response.data as Map<String, dynamic>;
if (results.containsKey("values")) {
data = results["values"] as Map<String, dynamic>;
}
}
}
// Return the entry associated with the provided integer status
Map<String, dynamic> entry(int status) {
for (String key in data.keys) {
dynamic _entry = data[key];
if (_entry is Map<String, dynamic>) {
dynamic _status = _entry["key"];
if (_status is int) {
if (status == _status) {
return _entry;
}
}
}
}
// No match - return an empty map
return {};
}
// Return the 'label' associated with a given status code
String label(int status) {
Map<String, dynamic> _entry = entry(status);
String _label = (_entry["label"] ?? "") as String;
if (_label.isEmpty) {
// If no match found, return the status code
debug("No match for status code ${status} at '${URL}'");
return status.toString();
} else {
return _label;
}
}
// Return the 'name' (untranslated) associated with a given status code
String name(int status) {
Map<String, dynamic> _entry = entry(status);
String _name = (_entry["name"] ?? "") as String;
if (_name.isEmpty) {
debug("No match for status code ${status} at '${URL}'");
}
return _name;
}
// Test if the name associated with the given code is in the provided list
bool isNameIn(int code, List<String> names) {
return names.contains(name(code));
}
// Return the 'color' associated with a given status code
Color color(int status) {
Map<String, dynamic> _entry = entry(status);
String color_name = (_entry["color"] ?? "") as String;
switch (color_name.toLowerCase()) {
case "success":
return COLOR_SUCCESS;
case "primary":
return COLOR_PROGRESS;
case "secondary":
return Colors.grey;
case "dark":
return Colors.black;
case "danger":
return COLOR_DANGER;
case "warning":
return COLOR_WARNING;
case "info":
return Colors.lightBlue;
default:
return Colors.black;
}
}
}

View file

@ -1,183 +1,182 @@
import "dart:async"; import "dart:async";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:inventree/api.dart"; import "package:intl/intl.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/stock/stock_detail.dart";
/*
* Class representing a test result for a single stock item
*/
class InvenTreeStockItemTestResult extends InvenTreeModel { class InvenTreeStockItemTestResult extends InvenTreeModel {
InvenTreeStockItemTestResult() : super(); InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String get URL => "stock/test/"; String get URL => "stock/test/";
@override @override
List<String> get rolesRequired => ["stock"]; Map<String, dynamic> formFields() {
return {
@override "stock_item": {
Map<String, String> defaultFilters() { "hidden": true
return {"user_detail": "true", "template_detail": "true"};
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"stock_item": {"hidden": true},
"test": {},
"template": {
"filters": {"enabled": "true"},
}, },
"test": {},
"result": {}, "result": {},
"value": {}, "value": {},
"notes": {}, "notes": {},
"attachment": {}, "attachment": {},
}; };
if (InvenTreeAPI().supportsModernTestResults) {
fields.remove("test");
} else {
fields.remove("template");
}
return fields;
} }
String get key => getString("key"); String get key => (jsondata["key"] ?? "") as String;
int get templateId => getInt("template"); String get testName => (jsondata["test"] ?? "") as String;
String get testName => getString("test"); bool get result => (jsondata["result"] ?? false) as bool;
bool get result => getBool("result"); String get value => (jsondata["value"] ?? "") as String;
String get value => getString("value"); String get attachment => (jsondata["attachment"] ?? "") as String;
String get attachment => getString("attachment"); String get date => (jsondata["date"] ?? "") as String;
String get username => getString("username", subKey: "user_detail");
String get date => getString("date");
@override @override
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) { InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
var result = InvenTreeStockItemTestResult.fromJson(json); var result = InvenTreeStockItemTestResult.fromJson(json);
return result; return result;
} }
} }
class InvenTreeStockItemHistory extends InvenTreeModel { class InvenTreeStockItemHistory extends InvenTreeModel {
InvenTreeStockItemHistory() : super(); InvenTreeStockItemHistory() : super();
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json) InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreeStockItemHistory.fromJson(json); return InvenTreeStockItemHistory.fromJson(json);
}
@override @override
String get URL => "stock/track/"; String get URL => "stock/track/";
@override @override
Map<String, String> defaultFilters() { Map<String, String> defaultListFilters() {
// By default, order by decreasing date // By default, order by decreasing date
return {"ordering": "-date", "user_detail": "true"}; return {
"ordering": "-date",
};
} }
DateTime? get date => getDate("date"); DateTime? get date {
if (jsondata.containsKey("date")) {
return DateTime.tryParse((jsondata["date"] ?? "") as String);
} else {
return null;
}
}
String get dateString => getDateString("date"); String get dateString {
var d = date;
String get label => getString("label"); if (d == null) {
return "";
}
// Return the "deltas" associated with this historical object return DateFormat("yyyy-MM-dd").format(d);
Map<String, dynamic> get deltas => getMap("deltas"); }
String get label => (jsondata["label"] ?? "") as String;
// Return the quantity string for this historical object
String get quantityString { String get quantityString {
var _deltas = deltas; Map<String, dynamic> deltas = (jsondata["deltas"] ?? {}) as Map<String, dynamic>;
if (_deltas.containsKey("quantity")) { // Serial number takes priority here
double q = double.tryParse(_deltas["quantity"].toString()) ?? 0; if (deltas.containsKey("serial")) {
var serial = (deltas["serial"] ?? "").toString();
return "# ${serial}";
} else if (deltas.containsKey("quantity")) {
double q = (deltas["quantity"] ?? 0) as double;
return simpleNumberString(q); return simpleNumberString(q);
} else { } else {
return ""; return "";
} }
} }
int? get user => getValue("user") as int?;
String get userString {
if (user != null) {
return getString("username", subKey: "user_detail");
} else {
return "";
}
}
} }
/*
* Class representing a StockItem database instance
*/
class InvenTreeStockItem extends InvenTreeModel { class InvenTreeStockItem extends InvenTreeModel {
InvenTreeStockItem() : super(); InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json); InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
// Stock status codes
static const int OK = 10;
static const int ATTENTION = 50;
static const int DAMAGED = 55;
static const int DESTROYED = 60;
static const int REJECTED = 65;
static const int LOST = 70;
static const int RETURNED = 85;
String statusLabel(BuildContext context) {
// TODO: Delete me - The translated status values are provided by the API!
switch (status) {
case OK:
return L10().ok;
case ATTENTION:
return L10().attention;
case DAMAGED:
return L10().damaged;
case DESTROYED:
return L10().destroyed;
case REJECTED:
return L10().rejected;
case LOST:
return L10().lost;
case RETURNED:
return L10().returned;
default:
return status.toString();
}
}
// Return color associated with stock status
Color get statusColor {
switch (status) {
case OK:
return Colors.black;
case ATTENTION:
return Color(0xFFfdc82a);
case DAMAGED:
case DESTROYED:
case REJECTED:
return Color(0xFFe35a57);
case LOST:
default:
return Color(0xFFAAAAAA);
}
}
@override @override
String get URL => "stock/"; String get URL => "stock/";
static const String MODEL_TYPE = "stockitem";
@override
List<String> get rolesRequired => ["stock"];
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => StockDetailWidget(this)),
);
}
// Return a set of fields to transfer this stock item via dialog
Map<String, dynamic> transferFields() {
Map<String, dynamic> fields = {
"pk": {"parent": "items", "nested": true, "hidden": true, "value": pk},
"quantity": {"parent": "items", "nested": true, "value": quantity},
"location": {"value": locationId},
"status": {"parent": "items", "nested": true, "value": status},
"packaging": {"parent": "items", "nested": true, "value": packaging},
"notes": {},
};
if (isSerialized()) {
// Prevent editing of 'quantity' field if the item is serialized
fields["quantity"]?["hidden"] = true;
}
// Old API does not support these fields
if (!api.supportsStockAdjustExtraFields) {
fields.remove("packaging");
fields.remove("status");
}
return fields;
}
// URLs for performing stock actions // URLs for performing stock actions
static String transferStockUrl() => "stock/transfer/"; static String transferStockUrl() => "stock/transfer/";
static String countStockUrl() => "stock/count/"; static String countStockUrl() => "stock/count/";
@ -190,32 +189,38 @@ class InvenTreeStockItem extends InvenTreeModel {
String get WEB_URL => "stock/item/"; String get WEB_URL => "stock/item/";
@override @override
Map<String, Map<String, dynamic>> formFields() { Map<String, dynamic> formFields() {
Map<String, Map<String, dynamic>> fields = { return {
"part": {}, "part": {},
"location": {}, "location": {},
"quantity": {}, "quantity": {},
"serial": {},
"serial_numbers": {"label": L10().serialNumbers, "type": "string"},
"status": {}, "status": {},
"batch": {}, "batch": {},
"purchase_price": {},
"purchase_price_currency": {},
"packaging": {}, "packaging": {},
"link": {}, "link": {},
}; };
return fields;
} }
@override @override
Map<String, String> defaultFilters() { Map<String, String> defaultGetFilters() {
return {
"part_detail": "true",
"location_detail": "true",
"supplier_detail": "true",
"cascade": "false"
};
}
@override
Map<String, String> defaultListFilters() {
return { return {
"part_detail": "true", "part_detail": "true",
"location_detail": "true", "location_detail": "true",
"supplier_detail": "true", "supplier_detail": "true",
"supplier_part_detail": "true",
"cascade": "false", "cascade": "false",
"in_stock": "true",
}; };
} }
@ -224,18 +229,20 @@ class InvenTreeStockItem extends InvenTreeModel {
int get testTemplateCount => testTemplates.length; int get testTemplateCount => testTemplates.length;
// Get all the test templates associated with this StockItem // Get all the test templates associated with this StockItem
Future<void> getTestTemplates({bool showDialog = false}) async { Future<void> getTestTemplates({bool showDialog=false}) async {
await InvenTreePartTestTemplate() await InvenTreePartTestTemplate().list(
.list(filters: {"part": "${partId}", "enabled": "true"}) filters: {
.then((var templates) { "part": "${partId}",
testTemplates.clear(); },
).then((var templates) {
testTemplates.clear();
for (var t in templates) { for (var t in templates) {
if (t is InvenTreePartTestTemplate) { if (t is InvenTreePartTestTemplate) {
testTemplates.add(t); testTemplates.add(t);
} }
} }
}); });
} }
List<InvenTreeStockItemTestResult> testResults = []; List<InvenTreeStockItemTestResult> testResults = [];
@ -243,86 +250,101 @@ class InvenTreeStockItem extends InvenTreeModel {
int get testResultCount => testResults.length; int get testResultCount => testResults.length;
Future<void> getTestResults() async { Future<void> getTestResults() async {
await InvenTreeStockItemTestResult()
.list(filters: {"stock_item": "${pk}", "user_detail": "true"})
.then((var results) {
testResults.clear();
for (var r in results) { await InvenTreeStockItemTestResult().list(
if (r is InvenTreeStockItemTestResult) { filters: {
testResults.add(r); "stock_item": "${pk}",
} "user_detail": "true",
} },
}); ).then((var results) {
testResults.clear();
for (var r in results) {
if (r is InvenTreeStockItemTestResult) {
testResults.add(r);
}
}
});
} }
bool get isInStock => getBool("in_stock", backup: true); String get uid => (jsondata["uid"] ?? "") as String;
String get packaging => getString("packaging"); int get status => (jsondata["status"] ?? -1) as int;
String get batch => getString("batch"); String get packaging => (jsondata["packaging"] ?? "") as String;
int get partId => getInt("part"); String get batch => (jsondata["batch"] ?? "") as String;
double? get purchasePrice { int get partId => (jsondata["part"] ?? -1) as int;
String pp = getString("purchase_price");
String get purchasePrice => (jsondata["purchase_price"] ?? "") as String;
if (pp.isEmpty) { bool get hasPurchasePrice {
return null;
String pp = purchasePrice;
return pp.isNotEmpty && pp.trim() != "-";
}
int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int;
int get trackingItemCount => (jsondata["tracking_items"] ?? 0) as int;
bool get isBuilding => (jsondata["is_building"] ?? false) as bool;
// Date of last update
DateTime? get updatedDate {
if (jsondata.containsKey("updated")) {
return DateTime.tryParse((jsondata["updated"] ?? "") as String);
} else { } else {
return double.tryParse(pp); return null;
} }
} }
String get purchasePriceCurrency => getString("purchase_price_currency"); String get updatedDateString {
var _updated = updatedDate;
bool get hasPurchasePrice { if (_updated == null) {
double? pp = purchasePrice; return "";
return pp != null && pp > 0; }
final DateFormat _format = DateFormat("yyyy-MM-dd");
return _format.format(_updated);
} }
int get purchaseOrderId => getInt("purchase_order"); DateTime? get stocktakeDate {
if (jsondata.containsKey("stocktake_date")) {
return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
} else {
return null;
}
}
int get trackingItemCount => getInt("tracking_items", backup: 0); String get stocktakeDateString {
var _stocktake = stocktakeDate;
bool get isBuilding => getBool("is_building"); if (_stocktake == null) {
return "";
}
int get salesOrderId => getInt("sales_order"); final DateFormat _format = DateFormat("yyyy-MM-dd");
bool get hasSalesOrder => salesOrderId > 0; return _format.format(_stocktake);
}
int get customerId => getInt("customer");
bool get hasCustomer => customerId > 0;
bool get stale => getBool("stale");
bool get expired => getBool("expired");
DateTime? get expiryDate => getDate("expiry_date");
String get expiryDateString => getDateString("expiry_date");
// Date of last update
DateTime? get updatedDate => getDate("updated");
String get updatedDateString => getDateString("updated");
DateTime? get stocktakeDate => getDate("stocktake_date");
String get stocktakeDateString => getDateString("stocktake_date");
String get partName { String get partName {
String nm = ""; String nm = "";
// Use the detailed part information as priority // Use the detailed part information as priority
if (jsondata.containsKey("part_detail")) { if (jsondata.containsKey("part_detail")) {
nm = (jsondata["part_detail"]?["full_name"] ?? "") as String; nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
} }
// Backup if first value fails // Backup if first value fails
if (nm.isEmpty) { if (nm.isEmpty) {
nm = getString("part__name"); nm = (jsondata["part__name"] ?? "") as String;
} }
return nm; return nm;
@ -333,11 +355,11 @@ class InvenTreeStockItem extends InvenTreeModel {
// Use the detailed part description as priority // Use the detailed part description as priority
if (jsondata.containsKey("part_detail")) { if (jsondata.containsKey("part_detail")) {
desc = (jsondata["part_detail"]?["description"] ?? "") as String; desc = (jsondata["part_detail"]["description"] ?? "") as String;
} }
if (desc.isEmpty) { if (desc.isEmpty) {
desc = getString("part__description"); desc = (jsondata["part__description"] ?? "") as String;
} }
return desc; return desc;
@ -347,11 +369,11 @@ class InvenTreeStockItem extends InvenTreeModel {
String img = ""; String img = "";
if (jsondata.containsKey("part_detail")) { if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String; img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
} }
if (img.isEmpty) { if (img.isEmpty) {
img = getString("part__thumbnail"); img = (jsondata["part__thumbnail"] ?? "") as String;
} }
return img; return img;
@ -361,6 +383,7 @@ class InvenTreeStockItem extends InvenTreeModel {
* Return the Part thumbnail for this stock item. * Return the Part thumbnail for this stock item.
*/ */
String get partThumbnail { String get partThumbnail {
String thumb = ""; String thumb = "";
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String; thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
@ -372,7 +395,7 @@ class InvenTreeStockItem extends InvenTreeModel {
// Try a different approach // Try a different approach
if (thumb.isEmpty) { if (thumb.isEmpty) {
thumb = getString("part__thumbnail"); thumb = (jsondata["part__thumbnail"] ?? "") as String;
} }
// Still no thumbnail? Use the "no image" image // Still no thumbnail? Use the "no image" image
@ -381,42 +404,49 @@ class InvenTreeStockItem extends InvenTreeModel {
return thumb; return thumb;
} }
int get supplierPartId => getInt("supplier_part"); int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
String get supplierImage { String get supplierImage {
String thumb = ""; String thumb = "";
if (jsondata.containsKey("supplier_part_detail")) { if (jsondata.containsKey("supplier_detail")) {
thumb = thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
(jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "")
as String;
} else if (jsondata.containsKey("supplier_detail")) {
thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
} }
return thumb; return thumb;
} }
String get supplierName => String get supplierName {
getString("supplier_name", subKey: "supplier_detail"); String sname = "";
String get units => getString("units", subKey: "part_detail"); if (jsondata.containsKey("supplier_detail")) {
sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
String get supplierSKU => getString("SKU", subKey: "supplier_part_detail");
String get serialNumber => getString("serial");
double get quantity => getDouble("quantity");
String quantityString({bool includeUnits = true}) {
String q = "";
if (allocated > 0) {
q += simpleNumberString(available);
q += " / ";
} }
q += simpleNumberString(quantity); return sname;
}
String get units {
return (jsondata["part_detail"]?["units"] ?? "") as String;
}
String get supplierSKU {
String sku = "";
if (jsondata.containsKey("supplier_detail")) {
sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
}
return sku;
}
String get serialNumber => (jsondata["serial"] ?? "") as String;
double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0;
String quantityString({bool includeUnits = false}){
String q = simpleNumberString(quantity);
if (includeUnits && units.isNotEmpty) { if (includeUnits && units.isNotEmpty) {
q += " ${units}"; q += " ${units}";
@ -425,45 +455,39 @@ class InvenTreeStockItem extends InvenTreeModel {
return q; return q;
} }
double get allocated => getDouble("allocated"); int get locationId => (jsondata["location"] ?? -1) as int;
double get available => quantity - allocated;
int get locationId => getInt("location");
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1; bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() { String serialOrQuantityDisplay() {
if (isSerialized()) { if (isSerialized()) {
return "SN ${serialNumber}"; return "SN ${serialNumber}";
} else if (allocated > 0) {
return "${available} / ${quantity}";
} else {
return simpleNumberString(quantity);
} }
return simpleNumberString(quantity);
} }
String get locationName { String get locationName {
if (locationId == -1 || !jsondata.containsKey("location_detail")) { String loc = "";
return "Unknown Location";
}
String loc = getString("name", subKey: "location_detail"); if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
loc = (jsondata["location_detail"]["name"] ?? "") as String;
// Old-style name // Old-style name
if (loc.isEmpty) { if (loc.isEmpty) {
loc = getString("location__name"); loc = (jsondata["location__name"] ?? "") as String;
} }
return loc; return loc;
} }
String get locationPathString { String get locationPathString {
if (locationId == -1 || !jsondata.containsKey("location_detail")) {
return L10().locationNotSet;
}
String _loc = getString("pathstring", subKey: "location_detail"); if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String;
if (_loc.isNotEmpty) { if (_loc.isNotEmpty) {
return _loc; return _loc;
} else { } else {
@ -477,19 +501,14 @@ class InvenTreeStockItem extends InvenTreeModel {
if (serialNumber.isNotEmpty) { if (serialNumber.isNotEmpty) {
return "SN: $serialNumber"; return "SN: $serialNumber";
} else { } else {
String q = simpleNumberString(quantity); return simpleNumberString(quantity);
if (units.isNotEmpty) {
q += " ${units}";
}
return q;
} }
} }
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreeStockItem.fromJson(json); return InvenTreeStockItem.fromJson(json);
}
/* /*
* Perform stocktake action: * Perform stocktake action:
@ -498,12 +517,9 @@ class InvenTreeStockItem extends InvenTreeModel {
* - Remove * - Remove
* - Count * - Count
*/ */
Future<bool> adjustStock( // TODO: Remove this function when we deprecate support for the old API
String endpoint, Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async {
double q, {
String? notes,
int? location,
}) async {
// Serialized stock cannot be adjusted (unless it is a "transfer") // Serialized stock cannot be adjusted (unless it is a "transfer")
if (isSerialized() && location == null) { if (isSerialized() && location == null) {
return false; return false;
@ -516,46 +532,72 @@ class InvenTreeStockItem extends InvenTreeModel {
Map<String, dynamic> data = {}; Map<String, dynamic> data = {};
data = { // Note: Format of adjustment API was updated in API v14
"items": [ if (InvenTreeAPI().supportModernStockTransactions()) {
{"pk": "${pk}", "quantity": "${quantity}"}, // Modern (> 14) API
], data = {
"notes": notes ?? "", "items": [
}; {
"pk": "${pk}",
"quantity": "${quantity}",
}
],
};
} else {
// Legacy (<= 14) API
data = {
"item": {
"pk": "${pk}",
"quantity": "${quantity}",
},
};
}
data["notes"] = notes ?? "";
if (location != null) { if (location != null) {
data["location"] = location; data["location"] = location;
} }
var response = await api.post(endpoint, body: data); // Expected API return code depends on server API version
final int expected_response = InvenTreeAPI().supportModernStockTransactions() ? 201 : 200;
return response.isValid() && var response = await api.post(
(response.statusCode == 200 || response.statusCode == 201); endpoint,
body: data,
expectedStatusCode: expected_response,
);
return response.isValid();
} }
Future<bool> countStock(double q, {String? notes}) async { // TODO: Remove this function when we deprecate support for the old API
final bool result = await adjustStock("/stock/count/", q, notes: notes); Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
return result; return result;
} }
Future<bool> addStock(double q, {String? notes}) async { // TODO: Remove this function when we deprecate support for the old API
final bool result = await adjustStock("/stock/add/", q, notes: notes); Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
return result; return result;
} }
Future<bool> removeStock(double q, {String? notes}) async { // TODO: Remove this function when we deprecate support for the old API
final bool result = await adjustStock("/stock/remove/", q, notes: notes); Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
return result; return result;
} }
Future<bool> transferStock( // TODO: Remove this function when we deprecate support for the old API
int location, { Future<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async {
double? quantity,
String? notes,
}) async {
double q = this.quantity; double q = this.quantity;
if (quantity != null) { if (quantity != null) {
@ -563,6 +605,7 @@ class InvenTreeStockItem extends InvenTreeModel {
} }
final bool result = await adjustStock( final bool result = await adjustStock(
context,
"/stock/transfer/", "/stock/transfer/",
q, q,
notes: notes, notes: notes,
@ -573,43 +616,29 @@ class InvenTreeStockItem extends InvenTreeModel {
} }
} }
class InvenTreeStockLocation extends InvenTreeModel { class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super(); InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
: super.fromJson(json);
@override @override
String get URL => "stock/location/"; String get URL => "stock/location/";
static const String MODEL_TYPE = "stocklocation"; String get pathstring => (jsondata["pathstring"] ?? "") as String;
@override @override
List<String> get rolesRequired => ["stock"]; Map<String, dynamic> formFields() {
return {
String get pathstring => getString("pathstring");
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => LocationDisplayWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"name": {}, "name": {},
"description": {}, "description": {},
"parent": {}, "parent": {},
"structural": {},
}; };
return fields;
} }
String get parentPathString { String get parentpathstring {
// TODO - Drive the refactor tractor through this
List<String> psplit = pathstring.split("/"); List<String> psplit = pathstring.split("/");
if (psplit.isNotEmpty) { if (psplit.isNotEmpty) {
@ -628,6 +657,10 @@ class InvenTreeStockLocation extends InvenTreeModel {
int get itemcount => (jsondata["items"] ?? 0) as int; int get itemcount => (jsondata["items"] ?? 0) as int;
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel createFromJson(Map<String, dynamic> json) {
InvenTreeStockLocation.fromJson(json);
} var loc = InvenTreeStockLocation.fromJson(json);
return loc;
}
}

View file

@ -1,18 +1,12 @@
import "package:inventree/l10n/collected/app_localizations.dart"; import "package:flutter_gen/gen_l10n/app_localizations.dart";
import "package:inventree/l10n/collected/app_localizations_en.dart"; import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
import "package:one_context/one_context.dart"; import "package:one_context/one_context.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:inventree/helpers.dart";
// Shortcut function to reduce boilerplate! // Shortcut function to reduce boilerplate!
I18N L10() { I18N L10()
// Testing mode - ignore context {
if (!hasContext()) {
return I18NEn();
}
BuildContext? _ctx = OneContext().context; BuildContext? _ctx = OneContext().context;
if (_ctx != null) { if (_ctx != null) {
@ -24,5 +18,5 @@ I18N L10() {
} }
// Fallback for "null" context // Fallback for "null" context
return I18NEn(); return I18NEn();
} }

1
lib/l10n Submodule

@ -0,0 +1 @@
Subproject commit a1683e462bb8c91d481cdacb169cb98d63e93acc

3
lib/l10n/.gitignore vendored
View file

@ -1,3 +0,0 @@
# Do not track the collected translation files
collected/
supported_locales.dart

View file

@ -1,23 +0,0 @@
## InvenTree Translation Files
This directory contains translation files for the InvenTree mobile app.
### File Structure
**Translation Source File** - app_en.arb
This file contains the source strings for translating. If you want to add a new translatable string to the app, is must be added to this file!
**Translated Files** - <lc>/app_<lb>arb
Each directory contains a single translation output file, generated by the [crowdin translation service](https://crowdin.com/project/inventree). *Do not edit these files*
**collected** - Collected files
Before building the app, the translation files are collected from the various directories into a single directory, so they can be accessed by the app.
### Translating
DO NOT EDIT THE TRANSLATION FILES DIRECTLY!
Translation files are crowd sourced using the [crowdin service](https://crowdin.com/project/inventree). Contributions are welcomed (and encouraged!)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,154 +0,0 @@
"""
Collect translation files into a single directory,
where they can be accessed by the flutter i18n library.
Translations provided from crowdin are located in subdirectories,
but we need the .arb files to appear in this top level directory
to be accessed by the app.
So, simply copy them here!
"""
import os
import glob
from posixpath import dirname
import shutil
import re
def process_locale_file(filename, locale_name):
"""
Process a locale file after copying
- Ensure the 'locale' matches
"""
# TODO: Use JSON processing instead of manual
# Need to work out unicode issues for this to work
with open(filename, "r", encoding="utf-8") as input_file:
lines = input_file.readlines()
with open(filename, "w", encoding="utf-8") as output_file:
# Using JSON processing would be simpler here,
# but it does not preserve unicode data!
for line in lines:
if "@@locale" in line:
new_line = f' "@@locale": "{locale_name}"'
if "," in line:
new_line += ","
new_line += "\n"
line = new_line
output_file.write(line)
def copy_locale_file(path):
"""
Locate and copy the locale file from the provided directory
"""
here = os.path.abspath(os.path.dirname(__file__))
for f in os.listdir(path):
src = os.path.join(path, f)
dst = os.path.join(here, "collected", f)
if os.path.exists(src) and os.path.isfile(src) and f.endswith(".arb"):
shutil.copyfile(src, dst)
print(f"Copied file '{f}'")
locale = os.path.split(path)[-1]
process_locale_file(dst, locale)
# Create a "fallback" locale file, without a country code specifier, if it does not exist
r = re.search(r"app_(\w+)_(\w+).arb", f)
locale = r.groups()[0]
fallback = f"app_{locale}.arb"
fallback_file = os.path.join(here, "collected", fallback)
if not os.path.exists(fallback_file):
print(f"Creating fallback file:", fallback_file)
shutil.copyfile(dst, fallback_file)
process_locale_file(fallback_file, locale)
def generate_locale_list(locales):
"""
Generate a .dart file which contains all the supported locales,
for importing into the project
"""
with open("supported_locales.dart", "w") as output:
output.write(
"// This file is auto-generated by the 'collect_translations.py' script - do not edit it directly!\n\n"
)
output.write("// dart format off\n\n")
output.write('import "package:flutter/material.dart";\n\n')
output.write("const List<Locale> supported_locales = [\n")
locales = sorted(locales)
for locale in locales:
if locale.startswith("."):
continue
splt = locale.split("_")
if len(splt) == 2:
lc, cc = splt
else:
lc = locale
cc = ""
output.write(
f' Locale("{lc}", "{cc}"), // Translations available in app_{locale}.arb\n'
)
output.write("];\n")
output.write("")
if __name__ == "__main__":
here = os.path.abspath(os.path.dirname(__file__))
# Ensure the 'collected' output directory exists
output_dir = os.path.join(here, "collected")
os.makedirs(output_dir, exist_ok=True)
# Remove existing .arb files from output directory
arbs = glob.glob(os.path.join(output_dir, "*.arb"))
for arb in arbs:
os.remove(arb)
locales = ["en"]
for locale in os.listdir(here):
# Ignore the output directory
if locale == "collected":
continue
f = os.path.join(here, locale)
if os.path.exists(f) and os.path.isdir(locale):
copy_locale_file(f)
locales.append(locale)
# Ensure the translation source file ('app_en.arb') is copied also
# Note that this does not require any further processing
src = os.path.join(here, "app_en.arb")
dst = os.path.join(here, "collected", "app_en.arb")
shutil.copyfile(src, dst)
generate_locale_list(locales)
print(f"Updated translations for {len(locales)} locales.")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more