Coffee
This commit is contained in:
@@ -102,6 +102,7 @@ def register():
|
||||
coffee.CoffeeCuppingCup,
|
||||
coffee.CoffeeCuppingResult,
|
||||
coffee.CoffeeCuppingResultLine,
|
||||
coffee.CoffeeLabAnalysis,
|
||||
coffee.PurchaseLine,
|
||||
coffee.SaleLine,
|
||||
duplicate.TradeCustomDuplicateStart,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,16 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="type">tree</field>
|
||||
<field name="name">coffee_cupping_result_line_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="coffee_lab_analysis_view_tree">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">coffee_lab_analysis_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="coffee_lab_analysis_view_form">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">coffee_lab_analysis_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_coffee_sample_form">
|
||||
<field name="name">Coffee Samples</field>
|
||||
@@ -147,11 +157,28 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="view" ref="coffee_cupping_criterion_view_form"/>
|
||||
<field name="act_window" ref="act_coffee_cupping_criterion_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_coffee_lab_analysis_form">
|
||||
<field name="name">Lab Analyses</field>
|
||||
<field name="res_model">coffee.lab.analysis</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_coffee_lab_analysis_form_view_tree">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="coffee_lab_analysis_view_tree"/>
|
||||
<field name="act_window" ref="act_coffee_lab_analysis_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_coffee_lab_analysis_form_view_form">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="coffee_lab_analysis_view_form"/>
|
||||
<field name="act_window" ref="act_coffee_lab_analysis_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Coffee" parent="purchase.menu_purchase" sequence="18"
|
||||
id="menu_coffee_purchase"/>
|
||||
<menuitem name="Samples" parent="menu_coffee_purchase" sequence="10"
|
||||
action="act_coffee_sample_form" id="menu_coffee_sample_purchase"/>
|
||||
<menuitem name="Lab Analyses" parent="menu_coffee_purchase"
|
||||
sequence="15" action="act_coffee_lab_analysis_form"
|
||||
id="menu_coffee_lab_analysis_purchase"/>
|
||||
<menuitem name="Cupping Sessions" parent="menu_coffee_purchase"
|
||||
sequence="20" action="act_coffee_cupping_session_form"
|
||||
id="menu_coffee_cupping_session_purchase"/>
|
||||
@@ -162,6 +189,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
id="menu_coffee_sale"/>
|
||||
<menuitem name="Samples" parent="menu_coffee_sale" sequence="10"
|
||||
action="act_coffee_sample_form" id="menu_coffee_sample_sale"/>
|
||||
<menuitem name="Lab Analyses" parent="menu_coffee_sale"
|
||||
sequence="15" action="act_coffee_lab_analysis_form"
|
||||
id="menu_coffee_lab_analysis_sale"/>
|
||||
<menuitem name="Cupping Sessions" parent="menu_coffee_sale"
|
||||
sequence="20" action="act_coffee_cupping_session_form"
|
||||
id="menu_coffee_cupping_session_sale"/>
|
||||
@@ -174,6 +204,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.ui.menu" id="menu_coffee_sample_purchase">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu" id="menu_coffee_lab_analysis_purchase">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu" id="menu_coffee_cupping_session_purchase">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
@@ -186,6 +219,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.ui.menu" id="menu_coffee_sample_sale">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu" id="menu_coffee_lab_analysis_sale">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu" id="menu_coffee_cupping_session_sale">
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
@@ -218,6 +254,11 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">reject</field>
|
||||
<field name="string">Reject</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_evaluate_quality_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">evaluate_quality</field>
|
||||
<field name="string">Evaluate quality</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_expire_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">expire</field>
|
||||
@@ -258,6 +299,36 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">draft</field>
|
||||
<field name="string">Draft</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_send_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">send</field>
|
||||
<field name="string">Send</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_receive_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">receive</field>
|
||||
<field name="string">Receive</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_accept_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">accept</field>
|
||||
<field name="string">Accept</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_reject_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">reject</field>
|
||||
<field name="string">Reject</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_cancel_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">cancel</field>
|
||||
<field name="string">Cancel</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_lab_analysis_draft_button">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="name">draft</field>
|
||||
<field name="string">Draft</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_coffee_sample">
|
||||
<field name="model">coffee.sample</field>
|
||||
@@ -308,5 +379,12 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_coffee_lab_analysis">
|
||||
<field name="model">coffee.lab.analysis</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -114,3 +114,57 @@ defect count and minimum cup score.
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## 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:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">analysis lifecycle: draft, sent, received, accepted, rejected, cancelled;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">request, sent, due and received dates;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">lab report reference;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">physical and organoleptic results: moisture, screen size, defect count and cup score;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">compliance check against the target purchase/sale line thresholds.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
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:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">the target line quality thresholds;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">the latest non-cancelled lab analysis;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">the average cupping results;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">cupping session decisions, including <code>re-cup</code> and <code>rejected</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
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.
|
||||
|
||||
@@ -115,3 +115,57 @@ maximum, nombre de defauts maximum et score cup minimum.
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## 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:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">cycle de vie analyse: draft, sent, received, accepted, rejected, cancelled;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">dates de demande, envoi, echeance et reception;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">reference de rapport labo;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">resultats physiques et organoleptiques: humidite, screen size, nombre de defauts, cup score;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">controle de conformite par rapport aux seuils de la ligne purchase/sale.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
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:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">les seuils qualite de la ligne cible;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">la derniere analyse labo non annulee;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">les resultats moyens de cupping;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">les decisions de seance de cupping, dont <code>re-cup</code> et <code>rejected</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
55
modules/purchase_trade/view/coffee_lab_analysis_form.xml
Normal file
55
modules/purchase_trade/view/coffee_lab_analysis_form.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0"?>
|
||||
<form col="12" col_widths="1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr">
|
||||
<group id="lab_summary" colspan="12" col="8" panel="summary"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr,min-content,1fr,min-content,1fr">
|
||||
<label name="sample"/>
|
||||
<field name="sample"/>
|
||||
<label name="lab"/>
|
||||
<field name="lab"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<label name="compliant"/>
|
||||
<field name="compliant"/>
|
||||
</group>
|
||||
<button name="send" string="Send"/>
|
||||
<button name="receive" string="Receive"/>
|
||||
<button name="accept" string="Accept"/>
|
||||
<button name="reject" string="Reject"/>
|
||||
<button name="cancel" string="Cancel"/>
|
||||
<button name="draft" string="Draft"/>
|
||||
<newline/>
|
||||
<group id="lab_dates" string="Dates"
|
||||
colspan="6" col="4" panel="card" icon="tryton-date"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="requested_date"/>
|
||||
<field name="requested_date"/>
|
||||
<label name="sent_date"/>
|
||||
<field name="sent_date"/>
|
||||
<label name="due_date"/>
|
||||
<field name="due_date"/>
|
||||
<label name="received_date"/>
|
||||
<field name="received_date"/>
|
||||
</group>
|
||||
<group id="lab_results" string="Results"
|
||||
colspan="6" col="4" panel="card" icon="tryton-note"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="report_reference"/>
|
||||
<field name="report_reference"/>
|
||||
<label name="moisture"/>
|
||||
<field name="moisture" width="120" xexpand="0" xfill="0"/>
|
||||
<label name="screen_size"/>
|
||||
<field name="screen_size"/>
|
||||
<label name="defect_count"/>
|
||||
<field name="defect_count" width="120" xexpand="0" xfill="0"/>
|
||||
<label name="cup_score"/>
|
||||
<field name="cup_score" width="120" xexpand="0" xfill="0"/>
|
||||
</group>
|
||||
<group id="lab_notes" string="Summary"
|
||||
colspan="12" col="1" panel="card" icon="tryton-note"
|
||||
xalign="0" yalign="0" col_widths="1fr">
|
||||
<field name="result_summary"/>
|
||||
</group>
|
||||
</form>
|
||||
14
modules/purchase_trade/view/coffee_lab_analysis_tree.xml
Normal file
14
modules/purchase_trade/view/coffee_lab_analysis_tree.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<tree>
|
||||
<field name="sample"/>
|
||||
<field name="lab"/>
|
||||
<field name="state"/>
|
||||
<field name="sent_date"/>
|
||||
<field name="due_date"/>
|
||||
<field name="received_date"/>
|
||||
<field name="report_reference"/>
|
||||
<field name="moisture"/>
|
||||
<field name="defect_count"/>
|
||||
<field name="cup_score"/>
|
||||
<field name="compliant"/>
|
||||
</tree>
|
||||
@@ -1,83 +1,121 @@
|
||||
<?xml version="1.0"?>
|
||||
<form col="4">
|
||||
<label name="reference"/>
|
||||
<field name="reference"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<newline/>
|
||||
<form col="12" col_widths="1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr,1fr">
|
||||
<group id="sample_summary" colspan="12" col="8" panel="summary"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr,min-content,1fr,min-content,1fr">
|
||||
<label name="reference"/>
|
||||
<field name="reference"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<label name="quality_status"/>
|
||||
<field name="quality_status"/>
|
||||
<label name="recommended_decision"/>
|
||||
<field name="recommended_decision"/>
|
||||
</group>
|
||||
<button name="receive" string="Receive"/>
|
||||
<button name="send_to_lab" string="Send to lab"/>
|
||||
<button name="review" string="Review"/>
|
||||
<button name="evaluate_quality" string="Evaluate quality"/>
|
||||
<button name="approve" string="Approve"/>
|
||||
<button name="reject" string="Reject"/>
|
||||
<button name="expire" string="Expire"/>
|
||||
<button name="archive" string="Archive"/>
|
||||
<button name="reset_to_requested" string="Reset"/>
|
||||
<newline/>
|
||||
<label name="direction"/>
|
||||
<field name="direction"/>
|
||||
<label name="sample_type"/>
|
||||
<field name="sample_type"/>
|
||||
<label name="purchase_line"/>
|
||||
<field name="purchase_line"/>
|
||||
<label name="sale_line"/>
|
||||
<field name="sale_line"/>
|
||||
<label name="lot"/>
|
||||
<field name="lot"/>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
<label name="public_origin"/>
|
||||
<field name="public_origin"/>
|
||||
<label name="public_coffee_type"/>
|
||||
<field name="public_coffee_type"/>
|
||||
<newline/>
|
||||
<separator string="Lifecycle" id="lifecycle" colspan="4"/>
|
||||
<label name="requested_date"/>
|
||||
<field name="requested_date"/>
|
||||
<label name="received_date"/>
|
||||
<field name="received_date"/>
|
||||
<label name="sent_date"/>
|
||||
<field name="sent_date"/>
|
||||
<label name="review_date"/>
|
||||
<field name="review_date"/>
|
||||
<label name="decision_date"/>
|
||||
<field name="decision_date"/>
|
||||
<label name="shelf_life_days"/>
|
||||
<field name="shelf_life_days"/>
|
||||
<label name="expiry_date"/>
|
||||
<field name="expiry_date"/>
|
||||
<label name="expires_soon"/>
|
||||
<field name="expires_soon"/>
|
||||
<newline/>
|
||||
<separator string="Sample" id="sample" colspan="4"/>
|
||||
<label name="quantity"/>
|
||||
<field name="quantity"/>
|
||||
<label name="unit"/>
|
||||
<field name="unit"/>
|
||||
<label name="storage_location"/>
|
||||
<field name="storage_location"/>
|
||||
<label name="delegated_to_lab"/>
|
||||
<field name="delegated_to_lab"/>
|
||||
<label name="lab"/>
|
||||
<field name="lab"/>
|
||||
<label name="lab_due_date"/>
|
||||
<field name="lab_due_date"/>
|
||||
<newline/>
|
||||
<separator string="Physical quality" id="physical_quality" colspan="4"/>
|
||||
<label name="moisture"/>
|
||||
<field name="moisture"/>
|
||||
<label name="screen_size"/>
|
||||
<field name="screen_size"/>
|
||||
<label name="defect_count"/>
|
||||
<field name="defect_count"/>
|
||||
<newline/>
|
||||
<separator string="Cupping sessions" id="cupping" colspan="4"/>
|
||||
<field name="cupping_lines" colspan="4" mode="tree"
|
||||
view_ids="purchase_trade.coffee_cupping_session_sample_view_tree"/>
|
||||
<newline/>
|
||||
<separator string="Notes" id="notes" colspan="4"/>
|
||||
<field name="notes" colspan="4"/>
|
||||
<field name="decision_note" colspan="4"/>
|
||||
<group id="sample_identity" string="Sample identity"
|
||||
colspan="6" col="4" panel="card" icon="tryton-public"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="direction"/>
|
||||
<field name="direction"/>
|
||||
<label name="sample_type"/>
|
||||
<field name="sample_type"/>
|
||||
<label name="purchase_line"/>
|
||||
<field name="purchase_line"/>
|
||||
<label name="sale_line"/>
|
||||
<field name="sale_line"/>
|
||||
<label name="lot"/>
|
||||
<field name="lot"/>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
<label name="public_origin"/>
|
||||
<field name="public_origin"/>
|
||||
<label name="public_coffee_type"/>
|
||||
<field name="public_coffee_type"/>
|
||||
</group>
|
||||
<group id="sample_lifecycle" string="Lifecycle"
|
||||
colspan="6" col="4" panel="card" icon="tryton-date"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="requested_date"/>
|
||||
<field name="requested_date"/>
|
||||
<label name="received_date"/>
|
||||
<field name="received_date"/>
|
||||
<label name="sent_date"/>
|
||||
<field name="sent_date"/>
|
||||
<label name="review_date"/>
|
||||
<field name="review_date"/>
|
||||
<label name="decision_date"/>
|
||||
<field name="decision_date"/>
|
||||
<label name="shelf_life_days"/>
|
||||
<field name="shelf_life_days"/>
|
||||
<label name="expiry_date"/>
|
||||
<field name="expiry_date"/>
|
||||
<label name="expires_soon"/>
|
||||
<field name="expires_soon"/>
|
||||
</group>
|
||||
<group id="sample_storage" string="Sample storage"
|
||||
colspan="6" col="4" panel="card" icon="tryton-archive"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="quantity"/>
|
||||
<field name="quantity"/>
|
||||
<label name="unit"/>
|
||||
<field name="unit"/>
|
||||
<label name="storage_location"/>
|
||||
<field name="storage_location"/>
|
||||
</group>
|
||||
<group id="sample_quality" string="Quality values"
|
||||
colspan="6" col="4" panel="card" icon="tryton-note"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="moisture"/>
|
||||
<field name="moisture" width="120" xexpand="0" xfill="0"/>
|
||||
<label name="screen_size"/>
|
||||
<field name="screen_size"/>
|
||||
<label name="defect_count"/>
|
||||
<field name="defect_count" width="120" xexpand="0" xfill="0"/>
|
||||
<label name="cupping_average_score"/>
|
||||
<field name="cupping_average_score" width="120" xexpand="0" xfill="0"/>
|
||||
<label name="lab_analysis_status"/>
|
||||
<field name="lab_analysis_status"/>
|
||||
</group>
|
||||
<group id="sample_lab" string="Lab delegation"
|
||||
colspan="12" col="4" panel="card" icon="tryton-list"
|
||||
xalign="0" yalign="0"
|
||||
col_widths="min-content,1fr,min-content,1fr">
|
||||
<label name="delegated_to_lab"/>
|
||||
<field name="delegated_to_lab"/>
|
||||
<label name="lab"/>
|
||||
<field name="lab"/>
|
||||
<label name="lab_due_date"/>
|
||||
<field name="lab_due_date"/>
|
||||
<newline/>
|
||||
<field name="lab_analyses" colspan="4" mode="tree,form"
|
||||
view_ids="purchase_trade.coffee_lab_analysis_view_tree,purchase_trade.coffee_lab_analysis_view_form"/>
|
||||
</group>
|
||||
<group id="sample_cupping" string="Cupping sessions"
|
||||
colspan="12" col="1" panel="card" icon="tryton-list"
|
||||
xalign="0" yalign="0" col_widths="1fr">
|
||||
<field name="cupping_lines" colspan="1" mode="tree"
|
||||
view_ids="purchase_trade.coffee_cupping_session_sample_view_tree"/>
|
||||
</group>
|
||||
<group id="sample_notes" string="Notes"
|
||||
colspan="12" col="1" panel="card" icon="tryton-note"
|
||||
xalign="0" yalign="0" col_widths="1fr">
|
||||
<field name="notes"/>
|
||||
<field name="decision_note"/>
|
||||
</group>
|
||||
</form>
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
<field name="direction"/>
|
||||
<field name="sample_type"/>
|
||||
<field name="state"/>
|
||||
<field name="quality_status"/>
|
||||
<field name="recommended_decision"/>
|
||||
<field name="party"/>
|
||||
<field name="product"/>
|
||||
<field name="public_origin"/>
|
||||
<field name="public_coffee_type"/>
|
||||
<field name="received_date"/>
|
||||
<field name="expiry_date"/>
|
||||
<field name="lab_analysis_status"/>
|
||||
<field name="cupping_average_score"/>
|
||||
<field name="lab"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user