diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index ea5da59..22acbd7 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -102,6 +102,7 @@ def register(): coffee.CoffeeCuppingCup, coffee.CoffeeCuppingResult, coffee.CoffeeCuppingResultLine, + coffee.CoffeeLabAnalysis, coffee.PurchaseLine, coffee.SaleLine, duplicate.TradeCustomDuplicateStart, diff --git a/modules/purchase_trade/coffee.py b/modules/purchase_trade/coffee.py index f5d83da..56b7a10 100644 --- a/modules/purchase_trade/coffee.py +++ b/modules/purchase_trade/coffee.py @@ -117,12 +117,39 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): 'Lab due date', states={'invisible': ~Eval('delegated_to_lab')}, depends=['delegated_to_lab']) + lab_analyses = fields.One2Many( + 'coffee.lab.analysis', 'sample', 'Lab analyses') + lab_analysis_status = fields.Function(fields.Selection([ + (None, ''), + ('none', 'No lab analysis'), + ('pending', 'Pending'), + ('received', 'Received'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ], 'Lab analysis'), 'get_lab_analysis_status') moisture = fields.Numeric('Moisture %', digits=(5, 2)) screen_size = fields.Char('Screen size') defect_count = fields.Integer('Defects count') cupping_lines = fields.One2Many( 'coffee.cupping.session.sample', 'sample', 'Cupping sessions') + cupping_average_score = fields.Function( + fields.Numeric('Cupping score', digits=(5, 2)), + 'get_cupping_average_score') + quality_status = fields.Function(fields.Selection([ + (None, ''), + ('pending', 'Pending'), + ('pass', 'Pass'), + ('warning', 'Warning'), + ('fail', 'Fail'), + ], 'Quality status'), 'get_quality_status') + recommended_decision = fields.Function(fields.Selection([ + (None, ''), + ('pending', 'Pending'), + ('approve', 'Approve'), + ('re_cup', 'Re-cup'), + ('reject', 'Reject'), + ], 'Recommended decision'), 'get_recommended_decision') notes = fields.Text('Notes') decision_note = fields.Text('Decision note') @@ -136,6 +163,10 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): ('sent_to_lab', 'under_review'), ('under_review', 'approved'), ('under_review', 'rejected'), + ('received', 'approved'), + ('received', 'rejected'), + ('sent_to_lab', 'approved'), + ('sent_to_lab', 'rejected'), ('requested', 'expired'), ('received', 'expired'), ('sent_to_lab', 'expired'), @@ -163,7 +194,13 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): 'depends': ['state'], }, 'reject': { - 'invisible': Eval('state') != 'under_review', + 'invisible': ~Eval('state').in_([ + 'received', 'sent_to_lab', 'under_review']), + 'depends': ['state'], + }, + 'evaluate_quality': { + 'invisible': ~Eval('state').in_([ + 'received', 'sent_to_lab', 'under_review']), 'depends': ['state'], }, 'expire': { @@ -269,6 +306,105 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): line = self.purchase_line or self.sale_line return getattr(line, 'coffee_type', None) + def _coffee_line(self): + return self.purchase_line or self.sale_line + + def _latest_lab_analysis(self): + analyses = [ + analysis for analysis in self.lab_analyses or [] + if analysis.state not in {'cancelled'}] + if not analyses: + return None + return sorted( + analyses, + key=lambda analysis: ( + analysis.received_date or analysis.sent_date + or analysis.requested_date or datetime.date.min, + getattr(analysis, 'id', 0) or 0), + reverse=True)[0] + + def get_lab_analysis_status(self, name=None): + analysis = self._latest_lab_analysis() + if not analysis: + return 'none' + if analysis.state in {'accepted', 'rejected'}: + return analysis.state + if analysis.state == 'received': + return 'received' + return 'pending' + + def _cupping_scores(self): + scores = [] + for line in self.cupping_lines or []: + score = line.get_average_score() + if score is not None: + scores.append(score) + return scores + + def get_cupping_average_score(self, name=None): + scores = self._cupping_scores() + if not scores: + return None + return sum(scores, Decimal(0)) / Decimal(len(scores)) + + def _quality_values(self): + analysis = self._latest_lab_analysis() + return { + 'moisture': ( + getattr(analysis, 'moisture', None) + if analysis and analysis.moisture is not None + else self.moisture), + 'defect_count': ( + getattr(analysis, 'defect_count', None) + if analysis and analysis.defect_count is not None + else self.defect_count), + 'cupping_score': ( + getattr(analysis, 'cup_score', None) + if analysis and analysis.cup_score is not None + else self.get_cupping_average_score()), + } + + def get_quality_status(self, name=None): + line = self._coffee_line() + values = self._quality_values() + checks = [] + moisture_max = getattr(line, 'coffee_moisture_max', None) + if moisture_max is not None and values['moisture'] is not None: + checks.append(values['moisture'] <= moisture_max) + defect_max = getattr(line, 'coffee_defect_max', None) + if defect_max is not None and values['defect_count'] is not None: + checks.append(values['defect_count'] <= defect_max) + score_min = getattr(line, 'coffee_cup_score_min', None) + if score_min is not None and values['cupping_score'] is not None: + checks.append(values['cupping_score'] >= score_min) + rejected_cupping = any( + cupping.decision == 'rejected' + for cupping in self.cupping_lines or []) + recup_cupping = any( + cupping.decision == 're_cup' + for cupping in self.cupping_lines or []) + if rejected_cupping: + return 'fail' + if recup_cupping: + return 'warning' + if not checks: + return 'pending' + if all(checks): + return 'pass' + if any(check is False for check in checks): + return 'fail' + return 'warning' + + def get_recommended_decision(self, name=None): + status = self.get_quality_status() + if status == 'pass': + return 'approve' + if status == 'fail': + return 'reject' + if status == 'warning': + return 're_cup' + return 'pending' + def get_is_expired(self, name=None): if self.state == 'expired': return True @@ -336,10 +472,28 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): @Workflow.transition('sent_to_lab') def send_to_lab(cls, samples): today = Pool().get('ir.date').today() + LabAnalysis = Pool().get('coffee.lab.analysis') cls.write(samples, { 'delegated_to_lab': True, 'sent_date': today, }) + values = [] + for sample in samples: + if not sample.lab: + continue + active_analyses = [ + analysis for analysis in sample.lab_analyses or [] + if analysis.state in {'draft', 'sent', 'received'}] + if active_analyses: + continue + values.append({ + 'sample': sample.id, + 'lab': sample.lab.id, + 'requested_date': today, + 'due_date': sample.lab_due_date, + }) + if values: + LabAnalysis.create(values) @classmethod @ModelView.button @@ -362,6 +516,34 @@ class CoffeeSample(Workflow, ModelSQL, ModelView): today = Pool().get('ir.date').today() cls.write(samples, {'decision_date': today}) + @classmethod + @ModelView.button + def evaluate_quality(cls, samples): + today = Pool().get('ir.date').today() + to_approve = [] + to_reject = [] + to_review = [] + for sample in samples: + decision = sample.get_recommended_decision() + if decision == 'approve': + to_approve.append(sample) + elif decision == 'reject': + to_reject.append(sample) + else: + to_review.append(sample) + if to_approve: + cls.write(to_approve, { + 'state': 'approved', + 'decision_date': today, + }) + if to_reject: + cls.write(to_reject, { + 'state': 'rejected', + 'decision_date': today, + }) + if to_review: + cls.write(to_review, {'state': 'under_review'}) + @classmethod @ModelView.button @Workflow.transition('expired') @@ -736,6 +918,159 @@ class CoffeeCuppingResultLine(ModelSQL, ModelView): notes = fields.Text('Notes') +class CoffeeLabAnalysis(Workflow, ModelSQL, ModelView): + 'Coffee Lab Analysis' + __name__ = 'coffee.lab.analysis' + + sample = fields.Many2One( + 'coffee.sample', 'Sample', required=True, ondelete='CASCADE') + lab = fields.Many2One('party.party', 'Lab', required=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('sent', 'Sent'), + ('received', 'Received'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ('cancelled', 'Cancelled'), + ], 'State', readonly=True, required=True) + requested_date = fields.Date('Requested Date') + sent_date = fields.Date('Sent Date') + due_date = fields.Date('Due Date') + received_date = fields.Date('Received Date') + report_reference = fields.Char('Report reference') + moisture = fields.Numeric('Moisture %', digits=(5, 2)) + screen_size = fields.Char('Screen size') + defect_count = fields.Integer('Defects count') + cup_score = fields.Numeric('Cup score', digits=(5, 2)) + compliant = fields.Function( + fields.Boolean('Compliant'), 'get_compliant') + result_summary = fields.Text('Result summary') + + @classmethod + def __setup__(cls): + super().__setup__() + cls._transitions |= set(( + ('draft', 'sent'), + ('sent', 'received'), + ('received', 'accepted'), + ('received', 'rejected'), + ('draft', 'cancelled'), + ('sent', 'cancelled'), + ('received', 'cancelled'), + ('cancelled', 'draft'), + )) + cls._buttons.update({ + 'send': { + 'invisible': Eval('state') != 'draft', + 'depends': ['state'], + }, + 'receive': { + 'invisible': Eval('state') != 'sent', + 'depends': ['state'], + }, + 'accept': { + 'invisible': Eval('state') != 'received', + 'depends': ['state'], + }, + 'reject': { + 'invisible': Eval('state') != 'received', + 'depends': ['state'], + }, + 'cancel': { + 'invisible': Eval('state').in_([ + 'accepted', 'rejected', 'cancelled']), + 'depends': ['state'], + }, + 'draft': { + 'invisible': Eval('state') != 'cancelled', + 'depends': ['state'], + }, + }) + + @staticmethod + def default_state(): + return 'draft' + + @staticmethod + def default_requested_date(): + return Pool().get('ir.date').today() + + def get_compliant(self, name=None): + if self.state == 'rejected': + return False + if self.state == 'accepted': + return True + sample = self.sample + if not sample: + return False + line = sample._coffee_line() + checks = [] + moisture_max = getattr(line, 'coffee_moisture_max', None) + if moisture_max is not None and self.moisture is not None: + checks.append(self.moisture <= moisture_max) + defect_max = getattr(line, 'coffee_defect_max', None) + if defect_max is not None and self.defect_count is not None: + checks.append(self.defect_count <= defect_max) + score_min = getattr(line, 'coffee_cup_score_min', None) + if score_min is not None and self.cup_score is not None: + checks.append(self.cup_score >= score_min) + return bool(checks and all(checks)) + + @classmethod + @ModelView.button + @Workflow.transition('sent') + def send(cls, analyses): + today = Pool().get('ir.date').today() + cls.write([a for a in analyses if not a.sent_date], { + 'sent_date': today, + }) + samples = [analysis.sample for analysis in analyses if analysis.sample] + if samples: + Pool().get('coffee.sample').write(samples, { + 'delegated_to_lab': True, + 'sent_date': today, + }) + + @classmethod + @ModelView.button + @Workflow.transition('received') + def receive(cls, analyses): + today = Pool().get('ir.date').today() + cls.write([a for a in analyses if not a.received_date], { + 'received_date': today, + }) + samples = [analysis.sample for analysis in analyses if analysis.sample] + if samples: + Pool().get('coffee.sample').write(samples, { + 'review_date': today, + 'state': 'under_review', + }) + + @classmethod + @ModelView.button + @Workflow.transition('accepted') + def accept(cls, analyses): + pass + + @classmethod + @ModelView.button + @Workflow.transition('rejected') + def reject(cls, analyses): + pass + + @classmethod + @ModelView.button + @Workflow.transition('cancelled') + def cancel(cls, analyses): + pass + + @classmethod + @ModelView.button + @Workflow.transition('draft') + def draft(cls, analyses): + pass + + class CoffeeLineMixin: active_coffee_compatibility = fields.Function( fields.Boolean('Active coffee compatibility'), @@ -749,6 +1084,7 @@ class CoffeeLineMixin: ('approved', 'Approved'), ('rejected', 'Rejected'), ('expired', 'Expired'), + ('warning', 'Warning'), ], 'Coffee quality'), 'get_coffee_quality_status') coffee_origin = fields.Char('Coffee origin') coffee_type = fields.Selection(COFFEE_TYPES, 'Coffee type') @@ -778,6 +1114,8 @@ class CoffeeLineMixin: return 'expired' if any(sample.state == 'approved' for sample in samples): return 'approved' + if any(sample.get_quality_status() == 'warning' for sample in samples): + return 'warning' return 'pending' diff --git a/modules/purchase_trade/coffee.xml b/modules/purchase_trade/coffee.xml index 2dbcdd2..1dddf7c 100644 --- a/modules/purchase_trade/coffee.xml +++ b/modules/purchase_trade/coffee.xml @@ -68,6 +68,16 @@ this repository contains the full copyright notices and license terms. --> tree coffee_cupping_result_line_tree + + coffee.lab.analysis + tree + coffee_lab_analysis_tree + + + coffee.lab.analysis + form + coffee_lab_analysis_form + Coffee Samples @@ -147,11 +157,28 @@ this repository contains the full copyright notices and license terms. --> + + Lab Analyses + coffee.lab.analysis + + + + + + + + + + + + @@ -162,6 +189,9 @@ this repository contains the full copyright notices and license terms. --> id="menu_coffee_sale"/> + @@ -174,6 +204,9 @@ this repository contains the full copyright notices and license terms. --> + + + @@ -186,6 +219,9 @@ this repository contains the full copyright notices and license terms. --> + + + @@ -218,6 +254,11 @@ this repository contains the full copyright notices and license terms. --> reject Reject + + coffee.sample + evaluate_quality + Evaluate quality + coffee.sample expire @@ -258,6 +299,36 @@ this repository contains the full copyright notices and license terms. --> draft Draft + + coffee.lab.analysis + send + Send + + + coffee.lab.analysis + receive + Receive + + + coffee.lab.analysis + accept + Accept + + + coffee.lab.analysis + reject + Reject + + + coffee.lab.analysis + cancel + Cancel + + + coffee.lab.analysis + draft + Draft + coffee.sample @@ -308,5 +379,12 @@ this repository contains the full copyright notices and license terms. --> + + coffee.lab.analysis + + + + + diff --git a/modules/purchase_trade/configuration.py b/modules/purchase_trade/configuration.py index d0d33ce..c846074 100644 --- a/modules/purchase_trade/configuration.py +++ b/modules/purchase_trade/configuration.py @@ -110,10 +110,12 @@ class Configuration(ModelSingleton, ModelSQL, ModelView): _COFFEE_MENU_XML_IDS = ( 'menu_coffee_purchase', 'menu_coffee_sample_purchase', + 'menu_coffee_lab_analysis_purchase', 'menu_coffee_cupping_session_purchase', 'menu_coffee_cupping_criterion_purchase', 'menu_coffee_sale', 'menu_coffee_sample_sale', + 'menu_coffee_lab_analysis_sale', 'menu_coffee_cupping_session_sale', 'menu_coffee_cupping_criterion_sale', ) diff --git a/modules/purchase_trade/docs/business/coffee.en.md b/modules/purchase_trade/docs/business/coffee.en.md index 22f2635..dda0863 100644 --- a/modules/purchase_trade/docs/business/coffee.en.md +++ b/modules/purchase_trade/docs/business/coffee.en.md @@ -114,3 +114,57 @@ defect count and minimum cup score. + +## BR-PT-COF-002 - Phase 2 lab delegation + +Status: active +Source: project decision on 2026-06-29 + +### Business rule + +A coffee sample can be delegated to a laboratory. The delegation creates and +tracks lab analyses linked to the sample: + +
    +
  • analysis lifecycle: draft, sent, received, accepted, rejected, cancelled; +
  • +
  • request, sent, due and received dates; +
  • +
  • lab report reference; +
  • +
  • physical and organoleptic results: moisture, screen size, defect count and cup score; +
  • +
  • compliance check against the target purchase/sale line thresholds. +
  • +
+ +The sample `Send to lab` button creates an active lab request when a lab is set +and no open analysis already exists. + +## BR-PT-COF-003 - Phase 3 consolidated quality decision + +Status: active +Source: project decision on 2026-06-29 + +### Business rule + +The quality decision of a sample is consolidated from: + +
    +
  • the target line quality thresholds; +
  • +
  • the latest non-cancelled lab analysis; +
  • +
  • the average cupping results; +
  • +
  • cupping session decisions, including re-cup and rejected. +
  • +
+ +The sample exposes a quality status (`pending`, `pass`, `warning`, `fail`) and +a recommended decision (`approve`, `re-cup`, `reject`). The `Evaluate quality` +button applies the recommendation: approval, rejection or review when the +sample must be cupped again. + +These features remain under `Active coffee compatibility` and do not add a +cross-flow blocking rule for non-coffee flows. diff --git a/modules/purchase_trade/docs/business/coffee.md b/modules/purchase_trade/docs/business/coffee.md index 4f7a415..e1b0a19 100644 --- a/modules/purchase_trade/docs/business/coffee.md +++ b/modules/purchase_trade/docs/business/coffee.md @@ -115,3 +115,57 @@ maximum, nombre de defauts maximum et score cup minimum. + +## BR-PT-COF-002 - Phase 2 delegation labo + +Statut: active +Source: decision projet du 2026-06-29 + +### Regle consultant + +Un echantillon cafe peut etre delegue a un laboratoire. La delegation cree et +pilote des analyses labo rattachees a l'echantillon: + +
    +
  • cycle de vie analyse: draft, sent, received, accepted, rejected, cancelled; +
  • +
  • dates de demande, envoi, echeance et reception; +
  • +
  • reference de rapport labo; +
  • +
  • resultats physiques et organoleptiques: humidite, screen size, nombre de defauts, cup score; +
  • +
  • controle de conformite par rapport aux seuils de la ligne purchase/sale. +
  • +
+ +Le bouton `Send to lab` de l'echantillon cree une demande labo active si un +labo est renseigne et s'il n'existe pas deja une analyse ouverte. + +## BR-PT-COF-003 - Phase 3 decision qualite consolidee + +Statut: active +Source: decision projet du 2026-06-29 + +### Regle consultant + +La decision qualite d'un echantillon est consolidee depuis: + +
    +
  • les seuils qualite de la ligne cible; +
  • +
  • la derniere analyse labo non annulee; +
  • +
  • les resultats moyens de cupping; +
  • +
  • les decisions de seance de cupping, dont re-cup et rejected. +
  • +
+ +L'echantillon expose un statut qualite (`pending`, `pass`, `warning`, `fail`) +et une decision recommandee (`approve`, `re-cup`, `reject`). Le bouton +`Evaluate quality` applique la recommandation: approbation, rejet ou retour en +revue si l'echantillon doit etre re-deguste. + +Ces fonctions restent sous `Active coffee compatibility` et n'ajoutent pas de +blocage transverse sur les flux non cafe. diff --git a/modules/purchase_trade/docs_source/business/coffee.en.md b/modules/purchase_trade/docs_source/business/coffee.en.md index e4e21b3..f2687b7 100644 --- a/modules/purchase_trade/docs_source/business/coffee.en.md +++ b/modules/purchase_trade/docs_source/business/coffee.en.md @@ -70,3 +70,45 @@ defect count and minimum cup score. - lab delegation is a tracking field, not a complete lab workflow yet; - coffee menus are inactive by default and synchronized when the configuration is saved. + +## BR-PT-COF-002 - Phase 2 lab delegation + +Status: active +Source: project decision on 2026-06-29 + +### Business rule + +A coffee sample can be delegated to a laboratory. The delegation creates and +tracks lab analyses linked to the sample: + +- analysis lifecycle: draft, sent, received, accepted, rejected, cancelled; +- request, sent, due and received dates; +- lab report reference; +- physical and organoleptic results: moisture, screen size, defect count and + cup score; +- compliance check against the target purchase/sale line thresholds. + +The sample `Send to lab` button creates an active lab request when a lab is set +and no open analysis already exists. + +## BR-PT-COF-003 - Phase 3 consolidated quality decision + +Status: active +Source: project decision on 2026-06-29 + +### Business rule + +The quality decision of a sample is consolidated from: + +- the target line quality thresholds; +- the latest non-cancelled lab analysis; +- the average cupping results; +- cupping session decisions, including `re-cup` and `rejected`. + +The sample exposes a quality status (`pending`, `pass`, `warning`, `fail`) and +a recommended decision (`approve`, `re-cup`, `reject`). The `Evaluate quality` +button applies the recommendation: approval, rejection or review when the +sample must be cupped again. + +These features remain under `Active coffee compatibility` and do not add a +cross-flow blocking rule for non-coffee flows. diff --git a/modules/purchase_trade/docs_source/business/coffee.md b/modules/purchase_trade/docs_source/business/coffee.md index 5b0bba5..57cbac5 100644 --- a/modules/purchase_trade/docs_source/business/coffee.md +++ b/modules/purchase_trade/docs_source/business/coffee.md @@ -75,3 +75,45 @@ maximum, nombre de defauts maximum et score cup minimum. complet; - les menus cafe sont inactifs par defaut et synchronises quand la configuration est sauvegardee. + +## BR-PT-COF-002 - Phase 2 delegation labo + +Statut: active +Source: decision projet du 2026-06-29 + +### Regle consultant + +Un echantillon cafe peut etre delegue a un laboratoire. La delegation cree et +pilote des analyses labo rattachees a l'echantillon: + +- cycle de vie analyse: draft, sent, received, accepted, rejected, cancelled; +- dates de demande, envoi, echeance et reception; +- reference de rapport labo; +- resultats physiques et organoleptiques: humidite, screen size, nombre de + defauts, cup score; +- controle de conformite par rapport aux seuils de la ligne purchase/sale. + +Le bouton `Send to lab` de l'echantillon cree une demande labo active si un +labo est renseigne et s'il n'existe pas deja une analyse ouverte. + +## BR-PT-COF-003 - Phase 3 decision qualite consolidee + +Statut: active +Source: decision projet du 2026-06-29 + +### Regle consultant + +La decision qualite d'un echantillon est consolidee depuis: + +- les seuils qualite de la ligne cible; +- la derniere analyse labo non annulee; +- les resultats moyens de cupping; +- les decisions de seance de cupping, dont `re-cup` et `rejected`. + +L'echantillon expose un statut qualite (`pending`, `pass`, `warning`, `fail`) +et une decision recommandee (`approve`, `re-cup`, `reject`). Le bouton +`Evaluate quality` applique la recommandation: approbation, rejet ou retour en +revue si l'echantillon doit etre re-deguste. + +Ces fonctions restent sous `Active coffee compatibility` et n'ajoutent pas de +blocage transverse sur les flux non cafe. diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 9d1529b..b096e3c 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -79,6 +79,54 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual([line.sample.reference for line in ordered], ['A', 'R']) + def test_coffee_lab_analysis_compliance_uses_line_targets(self): + 'coffee lab analysis compliance checks sample line targets' + line = SimpleNamespace( + coffee_moisture_max=Decimal('12.50'), + coffee_defect_max=8, + coffee_cup_score_min=Decimal('82.00')) + sample = SimpleNamespace(_coffee_line=lambda: line) + analysis = SimpleNamespace( + state='received', + sample=sample, + moisture=Decimal('12.00'), + defect_count=6, + cup_score=Decimal('84.00')) + + self.assertTrue( + coffee_module.CoffeeLabAnalysis.get_compliant(analysis)) + + def test_coffee_sample_quality_recommends_reject_on_failed_targets(self): + 'coffee sample quality recommendation rejects failed target values' + line = SimpleNamespace( + coffee_moisture_max=Decimal('12.50'), + coffee_defect_max=8, + coffee_cup_score_min=Decimal('82.00')) + sample = SimpleNamespace( + purchase_line=line, + sale_line=None, + moisture=Decimal('13.00'), + defect_count=6, + lab_analyses=[], + cupping_lines=[], + get_cupping_average_score=lambda: Decimal('84.00'), + _coffee_line=lambda: line, + _latest_lab_analysis=lambda: None, + _quality_values=lambda: { + 'moisture': Decimal('13.00'), + 'defect_count': 6, + 'cupping_score': Decimal('84.00'), + }) + sample.get_quality_status = ( + lambda: coffee_module.CoffeeSample.get_quality_status(sample)) + + self.assertEqual( + coffee_module.CoffeeSample.get_quality_status(sample), + 'fail') + self.assertEqual( + coffee_module.CoffeeSample.get_recommended_decision(sample), + 'reject') + def test_coffee_sample_default_expiry_date_uses_shelf_life(self): 'coffee sample expiry date defaults from request date and shelf life' values = { diff --git a/modules/purchase_trade/view/coffee_lab_analysis_form.xml b/modules/purchase_trade/view/coffee_lab_analysis_form.xml new file mode 100644 index 0000000..d2350e5 --- /dev/null +++ b/modules/purchase_trade/view/coffee_lab_analysis_form.xml @@ -0,0 +1,55 @@ + +
+ + +