feat(sales): local sales/purchase ledger (any currency)

A sale is a distinct model from a gift or a Plantare (reproduction
commitment): a recorded seed sale or purchase with an optional price in
ANY currency — €, Ğ1, time, or none yet. Mirrors the Plantare feature.

- schema v10: Sales table (SyncColumns), guarded createTable migration,
  schema dump + generated schema_v10 for the migration round-trip test
- enum SaleDirection { iSold, iBought }
- VarietyRepository: create/watch/watchForVariety/delete + backup
  export/exportForSync/import (LWW-by-HLC, tombstones)
- InventorySnapshot.sales + JSON codec encode/decode (round-trips amount,
  currency, counterparty)
- UI: SalesScreen (/sales), sale sheet, drawer entry, variety-detail
  action beside 'add Plantare'; money hides a trailing .0
- i18n sale block + menu.sales in en/es/pt/ast
- tests: sales repo test (5) incl. Ğ1/price-less/backup round-trip;
  Sales screen added to the small-screen overflow guard

Also harden the overflow guard: swallow the headless engine's image
resource service PNG-decode errors (unrelated to layout) while still
failing on real RenderFlex overflows, so home/about stop reporting
false failures.
This commit is contained in:
vjrj 2026-07-11 02:33:42 +02:00
parent de6938d5d7
commit 6de039d518
27 changed files with 6156 additions and 23 deletions

View file

@ -17,6 +17,7 @@ import 'package:tane/ui/inventory_list_screen.dart';
import 'package:tane/ui/market_screen.dart';
import 'package:tane/ui/plantares_screen.dart';
import 'package:tane/ui/profile_screen.dart';
import 'package:tane/ui/sales_screen.dart';
import 'package:tane/ui/settings_screen.dart';
import '../support/test_support.dart';
@ -51,6 +52,19 @@ void main() {
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
// flutter_test's headless engine can't decode our real PNG assets (the
// logo), so `Image.asset` throws from the "image resource service". That's
// unrelated to layout swallow ONLY those, and let every other FlutterError
// (crucially the "rendering library" RenderFlex overflow this guard exists
// to catch) still fail the test. Installed here, inside the running test,
// because testWidgets reinstalls its own onError at test start (a setUp
// override would be clobbered).
final previous = FlutterError.onError;
FlutterError.onError = (details) {
if (details.library == 'image resource service') return;
previous?.call(details);
};
addTearDown(() => FlutterError.onError = previous);
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
}
@ -258,6 +272,28 @@ void main() {
await disposeTree(tester);
});
testWidgets('the Sales screen (with a sale) fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
await repo.createSale(
direction: SaleDirection.iSold,
counterparty: 'Feria de intercambio de la comarca',
amount: 12.5,
currency: '',
);
await pumpSmall(
tester,
wrapScreen(
repository: repo,
locale: AppLocale.es,
child: const SalesScreen(),
),
);
await disposeTree(tester);
});
// NOTE: the variety-detail screen is deliberately NOT overflow-checked here.
// It reports a ~2px overflow on a RenderFlex that is already DISPOSED/DEFUNCT
// a transient stale frame while its cubit's Drift stream rebuilds during