diff --git a/modules/purchase_trade/docs/coffee_user_guide.en.html b/modules/purchase_trade/docs/coffee_user_guide.en.html new file mode 100644 index 0000000..a685d57 --- /dev/null +++ b/modules/purchase_trade/docs/coffee_user_guide.en.html @@ -0,0 +1,861 @@ + + + + + + User Guide - Coffee Features + + + +
+

User Guide - Coffee Features

+

+ This guide explains how to activate and use the coffee extensions in the CTRM: + coffee specifications on purchase and sale lines, sample tracking, laboratory + analyses, cupping sessions with blind cups, scoring criteria and quality decisions. +

+

Documented version: purchase_trade coffee implementation, updated 17/07/2026.

+
+ +
+ + +
+

1. Functional overview

+

+ The coffee functionality adds a quality workflow around physical purchase + and sale contracts. It is designed to keep the commercial contract, the + sample lifecycle, lab results and cupping decisions connected without + forcing non-coffee contracts to use any coffee-specific fields. +

+

The workflow is built around these objects:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ObjectPurposeWhere users usually access it
purchase.line / sale.lineStores the expected coffee profile and target quality thresholds.Purchase or Sale line, tab Coffee Quality.
coffee.sampleTracks a physical or logical sample linked to a contract line.From the line tab, or menu Coffee / Samples.
coffee.lab.analysisTracks a laboratory analysis requested for a sample.From a sample, or menu Coffee / Lab Analyses.
coffee.cupping.sessionGroups samples into a tasting session and generates blind cups.Menu Coffee / Cupping Sessions.
coffee.cupping.resultStores one cupper's evaluation for one cup.Inside a cup generated by a cupping session.
coffee.cupping.result.lineStores one scored criterion within a cupping result.Inside a result, in the Criteria list.
+
+ The coffee process is advisory by design. It calculates quality status, + recommended decisions and warnings, but the user remains responsible for + approving, rejecting or requesting another cup. +
+
+ +
+

2. Activating coffee mode

+

+ Coffee features are optional. They are hidden until the trade configuration + enables them. +

+
    +
  1. Open the trade configuration.
  2. +
  3. Go to the trade options area.
  4. +
  5. Enable Active coffee compatibility.
  6. +
  7. Save the configuration.
  8. +
+

Once enabled, the following menus are available under Purchase and Sale:

+

+ Coffee / Samples + Coffee / Lab Analyses + Coffee / Cupping Sessions + Coffee / Cupping Criteria +

+
+ If the option is disabled, coffee menus and the Coffee Quality page on + purchase and sale lines are removed from the UI. Existing data remains in + the database, but it is not shown to users. +
+
+ +
+

3. Coffee Quality tab on purchase and sale lines

+

+ The Coffee Quality tab on a trade line stores + the expected quality profile for the coffee and the thresholds used to + evaluate samples. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaMain fieldsHow it is used
Quality summaryCoffee quality, Coffee origin, Coffee type, Coffee processIdentifies the coffee and displays the aggregated quality state of linked samples.
Coffee identityVariety, Crop year, Screen sizeCompletes the commercial and physical description of the product.
Quality targetsMoisture max %, Defects max, Cup score minDefines the thresholds used by lab analysis and sample quality evaluation.
Coffee samplesOne2Many list of linked samplesCreates or reviews all samples linked to the trade line.
+

+ The line's Attributes Name can display the coffee attributes + in a compact way, for example origin, type, process, variety, crop, + screen, moisture, defects and cup score. +

+
+ +
+

4. Packing and market data on trade lines

+

+ The trade line also contains coffee operational fields in the general tab. + These fields are not part of the sample workflow, but they are useful for + physical quantity tracking and market hedging. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldMeaningOperational effect
PackagesNumber of bags, sacks or packages.Copied to the virtual lot as lot_qt when the line is validated.
Package unitUoM used to describe the package, for example Bag60 or Bag90.Copied to the virtual lot as lot_unit.
Packing descriptionFree text used for readable contract or logistics descriptions.Informational only.
Market referencePrice curve used as the market reference.Used by pricing, valuation and auto-hedging rules.
Market price / Market deltaMarket entry data used for pricing or hedge generation.May be copied to a generated derivative entry price.
+
+ The default virtual lot is created when the line is validated. The package + values are synchronized after creation and are also refreshed if the + packing values are later changed and the line is saved again. +
+
+ +
+

5. Coffee samples

+

+ A coffee sample can be created from the Coffee Quality + tab of a purchase or sale line, or from the menu + Coffee / Samples. Creating it from a line is + preferred because the direction and line link are filled automatically. +

+ +

Sample identity

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
ReferenceInternal or external sample reference.
DirectionPurchase or Sale. It is inferred when the sample is created from a trade line.
Sample TypeOffer, Pre-shipment, Shipment, Arrival, Stock, Customer or Retained sample.
Purchase Line / Sale LineThe contract line the sample belongs to.
LotOptional physical or virtual lot reference.
Product, Party, Origin, Coffee typeComputed from the linked trade line and used for sample review and cupping.
+ +

Storage and lifecycle

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
Requested DateDefaults to today when a sample is created.
Received DateFilled when the sample is received, or manually entered if needed.
Shelf life (days)Defaults to 90 days.
Expiry DateCalculated from requested or received date plus shelf life when not manually set.
Quantity, Unit, Sample locationUsed to track the physical sample stored in the office, lab or warehouse.
+ +

Sample workflow

+

+ Sample buttons are displayed depending on the current state. The typical + workflow is: +

+

+ Requested + Received + Sent to lab + Under review + Approved / Rejected + Expired + Archived +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ButtonAvailable fromEffect
ReceiveRequestedMoves the sample to received and fills Received Date if it is empty.
Send to labReceivedMoves the sample to sent to lab and creates a lab analysis if a lab is selected.
ReviewReceived or sent to labMoves the sample to quality review.
Evaluate qualityReceived, sent to lab or under reviewComputes the recommended decision from lab and cupping data.
Approve / RejectUnder review, or other reviewable states depending on the button visibilityRecords the final decision and decision date.
ExpireActive statesForces the sample into expired state.
ArchiveApproved, rejected or expiredMoves the sample out of active follow-up.
+
+ A sample is considered "expiring soon" when its expiry date is within the + next 14 days. Expired samples continue to affect the line's coffee quality + status until they are handled. +
+
+ +
+

6. Laboratory analyses

+

+ A lab analysis records the external or internal laboratory control of a + sample. It can be created from a sample or from the global lab analyses + menu. +

+ +

Creating a lab analysis from a sample

+
    +
  1. Open the sample.
  2. +
  3. Enable Delegated to lab if needed.
  4. +
  5. Select a Lab and optionally a Lab Due Date.
  6. +
  7. Click Send to lab.
  8. +
+

+ The system creates a new active lab analysis only if the sample does not + already have an active one. +

+ +

Lab analysis workflow

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StateMeaningCommon action
DraftThe request is being prepared.Fill lab, due date and requested date.
SentThe sample has been sent to the lab.Wait for the report.
ReceivedThe lab report has been received.Enter moisture, defects, cup score and summary.
AcceptedThe lab result is accepted.Use it for sample evaluation.
RejectedThe lab result is rejected or invalid.Request another analysis if needed.
+

+ The computed field Compliant compares the lab values against + the target thresholds on the linked purchase or sale line: +

+ +
+ +
+

7. Cupping sessions

+

+ A cupping session is a tasting event. It groups samples, prepares blind + cups and lets cuppers enter one or more results per cup. +

+ +

Before creating a session: define criteria

+

+ Go to Coffee / Cupping Criteria and create the + scoring criteria used by cuppers. Each criterion can have: +

+ +

+ Subtractive criteria are useful for defects or penalties. When a result + total is calculated, normal criteria add to the total and subtractive + criteria reduce it. +

+ +

Creating the session

+
    +
  1. Open Coffee / Cupping Sessions.
  2. +
  3. Create a new session.
  4. +
  5. Fill Name, Date, Responsible and Location.
  6. +
  7. Check Cup capacity. The default is 24.
  8. +
  9. Check Cups per sample. The default is 3.
  10. +
  11. Add samples to the Samples list.
  12. +
+

+ A session line is a link between one session and one sample. The line + also stores the sample's cupping decision: pending, approved, re-cup or + rejected. +

+ +

Adding a sample from a sample record

+

+ You can also open a coffee sample and go to the Cupping sessions + tab. When you add a row there, the sample is already known, but you must + choose the session. This creates the same session/sample link as adding + the sample from the cupping session screen. +

+ +

The session workflow

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StateButtonWhat happens
DraftPrepareChecks capacity, sorts samples and creates missing cups with blind codes.
PreparedStartMoves the session to in progress. Cuppers can enter results.
In progressDoneMarks the tasting session as completed.
Draft, Prepared or In progressCancelCancels the session.
CancelledDraftReopens the session in draft state.
+ +

What Prepare does

+

+ Prepare is the important operational step. The system: +

+ +

+ The default tasting order proposes arabicas first, blends and other + coffees next, and robustas at the end. +

+
+ If you change the number of cups per sample after preparation, running + Prepare again creates only the missing cups. Existing cups are not deleted. +
+ +

Blind information

+

+ Cuppers see operational tasting information such as origin and coffee type. + They do not need to see the full purchase/sale contract, supplier or + customer information during the tasting. +

+
+ +
+

8. Cupping results and criteria scoring

+

+ Once cups exist, users enter tasting results inside each cup. +

+ +

Data hierarchy

+
    +
  1. A Cupping Session contains session/sample lines.
  2. +
  3. Each session/sample line contains generated Cups.
  4. +
  5. Each cup can contain one or more Results.
  6. +
  7. Each result contains scored Criteria lines.
  8. +
+ +

Entering a result

+
    +
  1. Open a cupping session.
  2. +
  3. Open the relevant sample line.
  4. +
  5. Open the relevant cup.
  6. +
  7. Add a result.
  8. +
  9. Select the Cupper.
  10. +
  11. Enter the date if it differs from today.
  12. +
  13. Add criteria lines and enter scores.
  14. +
  15. Add notes if needed.
  16. +
+

+ The field Total score is computed from the result lines. It + is empty until at least one criterion has a score. +

+ +

Criteria lines

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldMeaning
CriterionThe scoring criterion configured in Coffee / Cupping Criteria.
ScoreThe numeric score entered by the cupper.
Defect foundMarks the criterion as identifying a defect.
NotesFree tasting notes for this criterion.
+ +

Score consolidation

+

+ Scores are consolidated upwards: +

+ +
+ A criteria line requires a criterion. If the Add button appears to do + nothing, check that the Criteria list is configured with both tree and + form views and that cupping criteria exist. +
+
+ +
+

9. Quality status and recommended decisions

+

+ Quality status is computed from direct sample values, lab analysis values + and cupping information. The system compares available values with the + thresholds stored on the purchase or sale line. +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IndicatorCalculation / logic
Quality statusPass, warning, fail or pending depending on available checks and cupping decisions.
Recommended decisionPass recommends approve, fail recommends reject, warning recommends re-cup, otherwise pending.
Lab analysis statusShows the status of the latest active lab analysis.
Cupping scoreUses the average cupping result when available.
+

+ Cupping session line decisions have a direct impact: +

+ +

+ On purchase and sale lines, the computed coffee quality summarizes all + linked samples. Rejected, expired or warning samples are therefore visible + directly on the contract line. +

+
+ +
+

10. Recommended daily routine

+
    +
  1. Review Coffee / Samples / Expiring soon.
  2. +
  3. Review Coffee / Samples / Pending.
  4. +
  5. Check lab analyses waiting for reports.
  6. +
  7. Create or prepare upcoming cupping sessions.
  8. +
  9. After cupping, enter result criteria and session/sample decisions.
  10. +
  11. After lab or cupping data is entered, use Evaluate quality on the sample.
  12. +
  13. Approve, reject or request a re-cup according to the quality process.
  14. +
  15. Archive completed samples when they no longer need active follow-up.
  16. +
+
+ A simple full flow is: create sample from the trade line, receive it, + send it to the lab and/or add it to a cupping session, enter results, + evaluate quality, then approve or reject. +
+
+ +
+

11. Troubleshooting and common questions

+ +

The Coffee Quality tab is not visible

+

+ Check that Active coffee compatibility is enabled in trade + configuration and that the view has been reloaded after saving. +

+ +

The Add button does nothing in a One2Many list

+

+ This usually means that the child record has another required parent + field that is not known from the current screen. Examples: +

+ +

+ The current views provide form popups for these cases. If the popup does + not appear, reload the client after updating the module views. +

+ +

No cups are created

+

+ Cups are created by the Prepare button on the cupping + session. Check that the session has sample lines and that the requested + number of cups does not exceed the session capacity. +

+ +

A sample remains pending

+

+ The system can only recommend a decision when it has enough data to + compare against the trade line thresholds. Check moisture, defects, + cupping score and lab analysis status. +

+ +

The result total does not match the simple sum

+

+ Some criteria may be configured as subtractive. In that case their score + is deducted from the total instead of added. +

+ +

Prepare blocks the session

+

+ The total requested cups are above Cup capacity. Increase + capacity, reduce Cups per sample, or remove some samples. +

+
+
+ + diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index ffab505..7b909ec 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -3268,6 +3268,46 @@ class PurchaseTradeTestCase(ModuleTestCase): 'shipment_in': shipment.id, }]) + def test_ppack_fee_valuation_uses_fee_packing_not_lot_packing(self): + 'per packing fee valuation keeps the fee quantity and amount' + Valuation = valuation_module.ValuationBase + fee = SimpleNamespace( + mode='ppack', + quantity=Decimal('2'), + price=Decimal('2800'), + amount=Decimal('5600'), + ) + lot = Mock(lot_qt=Decimal('440')) + segment = {'quantity': Decimal('26.4'), 'share': None} + + qty = Valuation._fee_segment_quantity(fee, lot, segment) + price, amount = Valuation._fee_segment_amount( + fee, lot, qty, Decimal('-1'), segment) + + self.assertEqual(qty, Decimal('2')) + self.assertEqual(price, Decimal('2800')) + self.assertEqual(amount, Decimal('-5600')) + + def test_ppack_fee_valuation_splits_fee_packing_by_segment_share(self): + 'per packing fee valuation prorates fee amount on segment shares' + Valuation = valuation_module.ValuationBase + fee = SimpleNamespace( + mode='ppack', + quantity=Decimal('2'), + price=Decimal('2800'), + amount=Decimal('5600'), + ) + lot = Mock(lot_qt=Decimal('440')) + segment = {'quantity': Decimal('13.2'), 'share': Decimal('0.5')} + + qty = Valuation._fee_segment_quantity(fee, lot, segment) + price, amount = Valuation._fee_segment_amount( + fee, lot, qty, Decimal('-1'), segment) + + self.assertEqual(qty, Decimal('1.00000')) + self.assertEqual(price, Decimal('2800')) + self.assertEqual(amount, Decimal('-2800.0')) + def test_snapshot_identity_prefers_sale_line_over_purchase_line(self): 'valuation snapshot identity ignores purchase line when sale line exists' Valuation = Pool().get('valuation.valuation') diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index adea88b..1d63b2c 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -1278,11 +1278,8 @@ class ValuationBase(ModelSQL): @classmethod def _fee_amount_for_lot_or_zero(cls, fee, lot): - if fee.mode == 'ppack' and getattr(lot, 'lot_qt', None): - return abs(round( - Decimal(fee.price or 0) - * Decimal(str(lot.lot_qt or 0)), - 2)) + if fee.mode == 'ppack': + return cls._fee_amount_or_zero(fee) if fee.mode == 'rate': line = fee.line or getattr(fee, 'sale_line', None) if line and line.estimated_date: @@ -1305,16 +1302,15 @@ class ValuationBase(ModelSQL): @classmethod def _fee_segment_quantity(cls, fee, lot, segment): quantity = segment.get('quantity') - if quantity is None: - qty = round(lot.get_current_quantity_converted(), 5) - if fee.mode == 'ppack' and getattr(lot, 'lot_qt', None): - qty = Decimal(str(lot.lot_qt or 0)) - return qty - if fee.mode == 'ppack' and getattr(lot, 'lot_qt', None): + if fee.mode == 'ppack': + fee_quantity = Decimal(str(getattr(fee, 'quantity', None) or 0)) share = segment.get('share') if share is not None: - return round(Decimal(str(lot.lot_qt or 0)) * share, 5) - return Decimal(str(lot.lot_qt or 0)) + return round(fee_quantity * share, 5) + return fee_quantity + if quantity is None: + qty = round(lot.get_current_quantity_converted(), 5) + return qty return round(Decimal(str(quantity)), 5) @classmethod