This commit is contained in:
2026-06-29 21:16:42 +02:00
parent f9ade37eab
commit f2947c9f86
25 changed files with 1015 additions and 94 deletions

View File

@@ -96,6 +96,12 @@ def register():
pricing.ImportPricesStart,
pricing.ImportPricesResult,
coffee.CoffeeSample,
coffee.CoffeeCuppingCriterion,
coffee.CoffeeCuppingSession,
coffee.CoffeeCuppingSessionSample,
coffee.CoffeeCuppingCup,
coffee.CoffeeCuppingResult,
coffee.CoffeeCuppingResultLine,
duplicate.TradeCustomDuplicateStart,
module='purchase_trade', type_='model')
Pool.register(

View File

@@ -6,6 +6,7 @@ from decimal import Decimal
from trytond.model import ModelSQL, ModelView, Workflow, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.exceptions import UserError
COFFEE_SAMPLE_TYPES = [
@@ -28,6 +29,25 @@ COFFEE_PROCESS_TYPES = [
('other', 'Other'),
]
COFFEE_TYPES = [
(None, ''),
('arabica', 'Arabica'),
('robusta', 'Robusta'),
('blend', 'Blend'),
('other', 'Other'),
]
def coffee_compatibility_active():
try:
Configuration = Pool().get('purchase_trade.configuration')
configurations = Configuration.search([], limit=1)
except Exception:
return False
return bool(
configurations
and configurations[0].active_coffee_compatibility)
class CoffeeSample(ModelSQL, ModelView, Workflow):
'Coffee Sample'
@@ -66,6 +86,10 @@ class CoffeeSample(ModelSQL, ModelView, Workflow):
fields.Many2One('product.product', 'Product'), 'get_product')
party = fields.Function(
fields.Many2One('party.party', 'Party'), 'get_party')
public_origin = fields.Function(
fields.Char('Origin'), 'get_public_origin')
public_coffee_type = fields.Function(
fields.Selection(COFFEE_TYPES, 'Coffee type'), 'get_public_coffee_type')
requested_date = fields.Date('Requested Date')
received_date = fields.Date('Received Date')
@@ -94,23 +118,11 @@ class CoffeeSample(ModelSQL, ModelView, Workflow):
states={'invisible': ~Eval('delegated_to_lab')},
depends=['delegated_to_lab'])
aroma = fields.Numeric('Aroma', digits=(5, 2))
flavor = fields.Numeric('Flavor', digits=(5, 2))
aftertaste = fields.Numeric('Aftertaste', digits=(5, 2))
acidity = fields.Numeric('Acidity', digits=(5, 2))
body = fields.Numeric('Body', digits=(5, 2))
balance = fields.Numeric('Balance', digits=(5, 2))
sweetness = fields.Numeric('Sweetness', digits=(5, 2))
clean_cup = fields.Numeric('Clean cup', digits=(5, 2))
uniformity = fields.Numeric('Uniformity', digits=(5, 2))
overall = fields.Numeric('Overall', digits=(5, 2))
defects = fields.Numeric('Defects', digits=(5, 2))
total_score = fields.Function(
fields.Numeric('Cup score', digits=(5, 2)), 'get_total_score')
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')
notes = fields.Text('Notes')
decision_note = fields.Text('Decision note')
@@ -249,21 +261,13 @@ class CoffeeSample(ModelSQL, ModelView, Workflow):
party = self.sale_line.sale.party
return party.id if party else None
def _score_fields(self):
return [
'aroma', 'flavor', 'aftertaste', 'acidity', 'body', 'balance',
'sweetness', 'clean_cup', 'uniformity', 'overall']
def get_public_origin(self, name=None):
line = self.purchase_line or self.sale_line
return getattr(line, 'coffee_origin', None)
def get_total_score(self, name=None):
total = Decimal(0)
for field_name in self._score_fields():
value = getattr(self, field_name, None)
if value is not None:
total += Decimal(str(value))
defects = getattr(self, 'defects', None)
if defects is not None:
total -= Decimal(str(defects))
return total
def get_public_coffee_type(self, name=None):
line = self.purchase_line or self.sale_line
return getattr(line, 'coffee_type', None)
def get_is_expired(self, name=None):
if self.state == 'expired':
@@ -377,7 +381,365 @@ class CoffeeSample(ModelSQL, ModelView, Workflow):
pass
class CoffeeCuppingCriterion(ModelSQL, ModelView):
'Coffee Cupping Criterion'
__name__ = 'coffee.cupping.criterion'
name = fields.Char('Name', required=True)
sequence = fields.Integer('Sequence')
active = fields.Boolean('Active')
kind = fields.Selection([
('score', 'Score'),
('defect', 'Defect'),
('text', 'Text'),
], 'Kind', required=True)
max_score = fields.Numeric('Max score', digits=(5, 2))
subtract_from_score = fields.Boolean('Subtract from score')
description = fields.Text('Description')
@staticmethod
def default_sequence():
return 10
@staticmethod
def default_active():
return True
@staticmethod
def default_kind():
return 'score'
@staticmethod
def default_max_score():
return Decimal('10.00')
class CoffeeCuppingSession(ModelSQL, ModelView, Workflow):
'Coffee Cupping Session'
__name__ = 'coffee.cupping.session'
name = fields.Char('Name', required=True)
date = fields.Date('Date')
responsible = fields.Many2One('party.party', 'Responsible')
location = fields.Char('Location')
cup_capacity = fields.Integer('Cup capacity')
cups_per_sample = fields.Integer('Cups per sample')
state = fields.Selection([
('draft', 'Draft'),
('prepared', 'Prepared'),
('in_progress', 'In progress'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], 'State', readonly=True, required=True)
lines = fields.One2Many(
'coffee.cupping.session.sample', 'session', 'Samples')
total_cups = fields.Function(
fields.Integer('Total cups'), 'get_total_cups')
remaining_cups = fields.Function(
fields.Integer('Remaining cups'), 'get_remaining_cups')
@classmethod
def __setup__(cls):
super().__setup__()
cls._transitions |= set((
('draft', 'prepared'),
('prepared', 'in_progress'),
('in_progress', 'done'),
('draft', 'cancelled'),
('prepared', 'cancelled'),
('in_progress', 'cancelled'),
('cancelled', 'draft'),
))
cls._buttons.update({
'prepare': {
'invisible': Eval('state') != 'draft',
'depends': ['state'],
},
'start': {
'invisible': Eval('state') != 'prepared',
'depends': ['state'],
},
'done': {
'invisible': Eval('state') != 'in_progress',
'depends': ['state'],
},
'cancel': {
'invisible': Eval('state').in_(['done', 'cancelled']),
'depends': ['state'],
},
'draft': {
'invisible': Eval('state') != 'cancelled',
'depends': ['state'],
},
})
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_date():
return Pool().get('ir.date').today()
@staticmethod
def default_cup_capacity():
return 24
@staticmethod
def default_cups_per_sample():
return 3
@classmethod
def _total_requested_cups(cls, lines):
total = 0
for line in lines:
session = getattr(line, 'session', None)
total += (
line.cups_per_sample
or getattr(session, 'cups_per_sample', 0)
or 0)
return total
def get_total_cups(self, name=None):
return self._total_requested_cups(self.lines or [])
def get_remaining_cups(self, name=None):
capacity = self.cup_capacity or 0
return capacity - self.get_total_cups()
@staticmethod
def _coffee_type_order(coffee_type):
return {
'arabica': 10,
'blend': 20,
'other': 30,
'robusta': 40,
}.get(coffee_type, 30)
@classmethod
def _line_sort_key(cls, line):
sample = getattr(line, 'sample', None)
coffee_type = sample.get_public_coffee_type() if sample else None
origin = sample.get_public_origin() if sample else ''
return (
cls._coffee_type_order(coffee_type),
origin or '',
getattr(sample, 'reference', '') or '',
getattr(sample, 'id', 0) or 0,
)
@classmethod
@ModelView.button
@Workflow.transition('prepared')
def prepare(cls, sessions):
Cup = Pool().get('coffee.cupping.cup')
Line = Pool().get('coffee.cupping.session.sample')
for session in sessions:
lines = list(session.lines or [])
total_cups = cls._total_requested_cups(lines)
if session.cup_capacity and total_cups > session.cup_capacity:
raise UserError(
'The cupping session exceeds the room cup capacity.')
for sequence, line in enumerate(
sorted(lines, key=cls._line_sort_key), 1):
if line.sequence != sequence:
Line.write([line], {'sequence': sequence})
existing_cups = list(line.cups or [])
missing = (line.cups_per_sample or session.cups_per_sample or 0)
missing -= len(existing_cups)
if missing <= 0:
continue
values = []
start = len(existing_cups) + 1
for cup_no in range(start, start + missing):
values.append({
'session_sample': line.id,
'cup_no': cup_no,
'blind_code': 'S%02d-C%d' % (sequence, cup_no),
})
Cup.create(values)
@classmethod
@ModelView.button
@Workflow.transition('in_progress')
def start(cls, sessions):
pass
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, sessions):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, sessions):
pass
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, sessions):
pass
class CoffeeCuppingSessionSample(ModelSQL, ModelView):
'Coffee Cupping Session Sample'
__name__ = 'coffee.cupping.session.sample'
session = fields.Many2One(
'coffee.cupping.session', 'Session', required=True,
ondelete='CASCADE')
sample = fields.Many2One(
'coffee.sample', 'Sample', required=True, ondelete='CASCADE')
sequence = fields.Integer('Order')
cups_per_sample = fields.Integer('Cups')
decision = fields.Selection([
('pending', 'Pending'),
('approved', 'Approved'),
('re_cup', 'Re-cup'),
('rejected', 'Rejected'),
], 'Decision', required=True)
decision_note = fields.Text('Decision note')
public_origin = fields.Function(
fields.Char('Origin'), 'get_public_origin')
public_coffee_type = fields.Function(
fields.Selection(COFFEE_TYPES, 'Coffee type'), 'get_public_coffee_type')
average_score = fields.Function(
fields.Numeric('Average score', digits=(5, 2)), 'get_average_score')
defect_count = fields.Function(
fields.Integer('Defect reports'), 'get_defect_count')
result_count = fields.Function(
fields.Integer('Results'), 'get_result_count')
cups = fields.One2Many(
'coffee.cupping.cup', 'session_sample', 'Cups')
@staticmethod
def default_decision():
return 'pending'
def get_public_origin(self, name=None):
return self.sample.get_public_origin() if self.sample else None
def get_public_coffee_type(self, name=None):
return self.sample.get_public_coffee_type() if self.sample else None
def _results(self):
results = []
for cup in self.cups or []:
results.extend(cup.results or [])
return results
def get_average_score(self, name=None):
scores = [
result.get_total_score()
for result in self._results()
if result.get_total_score() is not None]
if not scores:
return None
return sum(scores, Decimal(0)) / Decimal(len(scores))
def get_defect_count(self, name=None):
return sum(1 for result in self._results() if result.has_defect)
def get_result_count(self, name=None):
return len(self._results())
class CoffeeCuppingCup(ModelSQL, ModelView):
'Coffee Cupping Cup'
__name__ = 'coffee.cupping.cup'
session_sample = fields.Many2One(
'coffee.cupping.session.sample', 'Session sample',
required=True, ondelete='CASCADE')
cup_no = fields.Integer('Cup')
blind_code = fields.Char('Blind code', required=True)
public_origin = fields.Function(
fields.Char('Origin'), 'get_public_origin')
public_coffee_type = fields.Function(
fields.Selection(COFFEE_TYPES, 'Coffee type'), 'get_public_coffee_type')
average_score = fields.Function(
fields.Numeric('Average score', digits=(5, 2)), 'get_average_score')
defect_count = fields.Function(
fields.Integer('Defect reports'), 'get_defect_count')
results = fields.One2Many(
'coffee.cupping.result', 'cup', 'Results')
def get_public_origin(self, name=None):
if self.session_sample:
return self.session_sample.get_public_origin()
def get_public_coffee_type(self, name=None):
if self.session_sample:
return self.session_sample.get_public_coffee_type()
def get_average_score(self, name=None):
scores = [
result.get_total_score()
for result in self.results or []
if result.get_total_score() is not None]
if not scores:
return None
return sum(scores, Decimal(0)) / Decimal(len(scores))
def get_defect_count(self, name=None):
return sum(1 for result in self.results or [] if result.has_defect)
class CoffeeCuppingResult(ModelSQL, ModelView):
'Coffee Cupping Result'
__name__ = 'coffee.cupping.result'
cup = fields.Many2One(
'coffee.cupping.cup', 'Cup', required=True, ondelete='CASCADE')
cupper = fields.Many2One('party.party', 'Cupper', required=True)
date = fields.Date('Date')
has_defect = fields.Boolean('Defect found')
total_score = fields.Function(
fields.Numeric('Total score', digits=(5, 2)), 'get_total_score')
lines = fields.One2Many(
'coffee.cupping.result.line', 'result', 'Criteria')
notes = fields.Text('Notes')
@staticmethod
def default_date():
return Pool().get('ir.date').today()
def get_total_score(self, name=None):
total = Decimal(0)
has_score = False
for line in self.lines or []:
score = line.score
if score is None:
continue
has_score = True
if line.criterion and line.criterion.subtract_from_score:
total -= Decimal(str(score))
else:
total += Decimal(str(score))
return total if has_score else None
class CoffeeCuppingResultLine(ModelSQL, ModelView):
'Coffee Cupping Result Line'
__name__ = 'coffee.cupping.result.line'
result = fields.Many2One(
'coffee.cupping.result', 'Result', required=True,
ondelete='CASCADE')
criterion = fields.Many2One(
'coffee.cupping.criterion', 'Criterion', required=True)
score = fields.Numeric('Score', digits=(5, 2))
defect_found = fields.Boolean('Defect found')
notes = fields.Text('Notes')
class CoffeeLineMixin:
active_coffee_compatibility = fields.Function(
fields.Boolean('Active coffee compatibility'),
'on_change_with_active_coffee_compatibility')
coffee_samples = fields.One2Many(
'coffee.sample', None, 'Coffee samples')
coffee_quality_status = fields.Function(fields.Selection([
@@ -389,6 +751,7 @@ class CoffeeLineMixin:
('expired', 'Expired'),
], 'Coffee quality'), 'get_coffee_quality_status')
coffee_origin = fields.Char('Coffee origin')
coffee_type = fields.Selection(COFFEE_TYPES, 'Coffee type')
coffee_process = fields.Selection(COFFEE_PROCESS_TYPES, 'Coffee process')
coffee_variety = fields.Char('Coffee variety')
coffee_crop_year = fields.Char('Crop year')
@@ -401,6 +764,9 @@ class CoffeeLineMixin:
def _coffee_samples(self):
return list(getattr(self, 'coffee_samples', None) or [])
def on_change_with_active_coffee_compatibility(self, name=None):
return coffee_compatibility_active()
def get_coffee_quality_status(self, name=None):
samples = self._coffee_samples()
if not samples:

View File

@@ -13,10 +13,65 @@ this repository contains the full copyright notices and license terms. -->
<field name="type">form</field>
<field name="name">coffee_sample_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_criterion_view_tree">
<field name="model">coffee.cupping.criterion</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_criterion_tree</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_criterion_view_form">
<field name="model">coffee.cupping.criterion</field>
<field name="type">form</field>
<field name="name">coffee_cupping_criterion_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_session_view_tree">
<field name="model">coffee.cupping.session</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_session_tree</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_session_view_form">
<field name="model">coffee.cupping.session</field>
<field name="type">form</field>
<field name="name">coffee_cupping_session_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_session_sample_view_tree">
<field name="model">coffee.cupping.session.sample</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_session_sample_tree</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_session_sample_view_form">
<field name="model">coffee.cupping.session.sample</field>
<field name="type">form</field>
<field name="name">coffee_cupping_session_sample_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_cup_view_tree">
<field name="model">coffee.cupping.cup</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_cup_tree</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_cup_view_form">
<field name="model">coffee.cupping.cup</field>
<field name="type">form</field>
<field name="name">coffee_cupping_cup_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_result_view_tree">
<field name="model">coffee.cupping.result</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_result_tree</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_result_view_form">
<field name="model">coffee.cupping.result</field>
<field name="type">form</field>
<field name="name">coffee_cupping_result_form</field>
</record>
<record model="ir.ui.view" id="coffee_cupping_result_line_view_tree">
<field name="model">coffee.cupping.result.line</field>
<field name="type">tree</field>
<field name="name">coffee_cupping_result_line_tree</field>
</record>
<record model="ir.action.act_window" id="act_coffee_sample_form">
<field name="name">Coffee Samples</field>
<field name="res_model">coffee.sample</field>
<field name="search_value"></field>
</record>
<record model="ir.action.act_window.view" id="act_coffee_sample_form_view_tree">
<field name="sequence" eval="10"/>
@@ -63,18 +118,80 @@ this repository contains the full copyright notices and license terms. -->
<field name="act_window" ref="act_coffee_sample_form"/>
</record>
<menuitem
name="Coffee Samples"
parent="purchase.menu_purchase"
sequence="18"
action="act_coffee_sample_form"
id="menu_coffee_sample_purchase"/>
<menuitem
name="Coffee Samples"
parent="sale.menu_sale"
sequence="18"
action="act_coffee_sample_form"
id="menu_coffee_sample_sale"/>
<record model="ir.action.act_window" id="act_coffee_cupping_session_form">
<field name="name">Cupping Sessions</field>
<field name="res_model">coffee.cupping.session</field>
</record>
<record model="ir.action.act_window.view" id="act_coffee_cupping_session_form_view_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="coffee_cupping_session_view_tree"/>
<field name="act_window" ref="act_coffee_cupping_session_form"/>
</record>
<record model="ir.action.act_window.view" id="act_coffee_cupping_session_form_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="coffee_cupping_session_view_form"/>
<field name="act_window" ref="act_coffee_cupping_session_form"/>
</record>
<record model="ir.action.act_window" id="act_coffee_cupping_criterion_form">
<field name="name">Cupping Criteria</field>
<field name="res_model">coffee.cupping.criterion</field>
</record>
<record model="ir.action.act_window.view" id="act_coffee_cupping_criterion_form_view_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="coffee_cupping_criterion_view_tree"/>
<field name="act_window" ref="act_coffee_cupping_criterion_form"/>
</record>
<record model="ir.action.act_window.view" id="act_coffee_cupping_criterion_form_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="coffee_cupping_criterion_view_form"/>
<field name="act_window" ref="act_coffee_cupping_criterion_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="Cupping Sessions" parent="menu_coffee_purchase"
sequence="20" action="act_coffee_cupping_session_form"
id="menu_coffee_cupping_session_purchase"/>
<menuitem name="Cupping Criteria" parent="menu_coffee_purchase"
sequence="30" action="act_coffee_cupping_criterion_form"
id="menu_coffee_cupping_criterion_purchase"/>
<menuitem name="Coffee" parent="sale.menu_sale" sequence="18"
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="Cupping Sessions" parent="menu_coffee_sale"
sequence="20" action="act_coffee_cupping_session_form"
id="menu_coffee_cupping_session_sale"/>
<menuitem name="Cupping Criteria" parent="menu_coffee_sale"
sequence="30" action="act_coffee_cupping_criterion_form"
id="menu_coffee_cupping_criterion_sale"/>
<record model="ir.ui.menu" id="menu_coffee_purchase">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_sample_purchase">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_cupping_session_purchase">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_cupping_criterion_purchase">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_sale">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_sample_sale">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_cupping_session_sale">
<field name="active" eval="False"/>
</record>
<record model="ir.ui.menu" id="menu_coffee_cupping_criterion_sale">
<field name="active" eval="False"/>
</record>
<record model="ir.model.button" id="coffee_sample_receive_button">
<field name="model">coffee.sample</field>
@@ -116,6 +233,31 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">reset_to_requested</field>
<field name="string">Reset</field>
</record>
<record model="ir.model.button" id="coffee_cupping_session_prepare_button">
<field name="model">coffee.cupping.session</field>
<field name="name">prepare</field>
<field name="string">Prepare</field>
</record>
<record model="ir.model.button" id="coffee_cupping_session_start_button">
<field name="model">coffee.cupping.session</field>
<field name="name">start</field>
<field name="string">Start</field>
</record>
<record model="ir.model.button" id="coffee_cupping_session_done_button">
<field name="model">coffee.cupping.session</field>
<field name="name">done</field>
<field name="string">Done</field>
</record>
<record model="ir.model.button" id="coffee_cupping_session_cancel_button">
<field name="model">coffee.cupping.session</field>
<field name="name">cancel</field>
<field name="string">Cancel</field>
</record>
<record model="ir.model.button" id="coffee_cupping_session_draft_button">
<field name="model">coffee.cupping.session</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>
@@ -124,9 +266,43 @@ 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_sample_purchase">
<field name="model">coffee.sample</field>
<field name="group" ref="purchase.group_purchase"/>
<record model="ir.model.access" id="access_coffee_cupping_criterion">
<field name="model">coffee.cupping.criterion</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>
<record model="ir.model.access" id="access_coffee_cupping_session">
<field name="model">coffee.cupping.session</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>
<record model="ir.model.access" id="access_coffee_cupping_session_sample">
<field name="model">coffee.cupping.session.sample</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>
<record model="ir.model.access" id="access_coffee_cupping_cup">
<field name="model">coffee.cupping.cup</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>
<record model="ir.model.access" id="access_coffee_cupping_result">
<field name="model">coffee.cupping.result</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>
<record model="ir.model.access" id="access_coffee_cupping_result_line">
<field name="model">coffee.cupping.result.line</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>

View File

@@ -107,8 +107,20 @@ class Configuration(ModelSingleton, ModelSQL, ModelView):
('shipment_packing_list_report_label', 'purchase_trade',
'report_shipment_in_packing_list', 'Packing List'),
)
_COFFEE_MENU_XML_IDS = (
'menu_coffee_purchase',
'menu_coffee_sample_purchase',
'menu_coffee_cupping_session_purchase',
'menu_coffee_cupping_criterion_purchase',
'menu_coffee_sale',
'menu_coffee_sample_sale',
'menu_coffee_cupping_session_sale',
'menu_coffee_cupping_criterion_sale',
)
pricing_rule = fields.Text("Pricing Rule")
active_coffee_compatibility = fields.Boolean(
"Active coffee compatibility")
sale_report_template = fields.Char("Sale Template")
sale_report_label = fields.Char("Sale Menu Label")
sale_commission_report_template = fields.Char(
@@ -149,16 +161,23 @@ class Configuration(ModelSingleton, ModelSQL, ModelView):
shipment_packing_list_report_label = fields.Char(
"Packing List Menu Label")
@classmethod
def default_active_coffee_compatibility(cls):
return False
@classmethod
def create(cls, vlist):
records = super().create(vlist)
cls._sync_report_labels(records)
cls._sync_coffee_menu_visibility(records)
return records
@classmethod
def write(cls, *args):
super().write(*args)
cls._sync_report_labels(sum(args[::2], []))
records = sum(args[::2], [])
cls._sync_report_labels(records)
cls._sync_coffee_menu_visibility(records)
@classmethod
def _sync_report_labels(cls, records):
@@ -179,3 +198,24 @@ class Configuration(ModelSingleton, ModelSQL, ModelView):
if to_write:
with Transaction().set_user(0):
ActionReport.write(*to_write)
@classmethod
def _sync_coffee_menu_visibility(cls, records):
if not records:
return
pool = Pool()
ModelData = pool.get('ir.model.data')
Menu = pool.get('ir.ui.menu')
active = any(
bool(record.active_coffee_compatibility)
for record in records)
menus = []
for xml_id in cls._COFFEE_MENU_XML_IDS:
try:
menu_id = ModelData.get_id('purchase_trade', xml_id)
except Exception:
continue
menus.append(Menu(menu_id))
if menus:
with Transaction().set_user(0):
Menu.write(menus, {'active': active})

View File

@@ -24,7 +24,31 @@ Each sample stores:
</li>
<li style="margin:0.38rem 0;">an expiry date computed from the request/receipt date and the sample shelf life;
</li>
<li style="margin:0.38rem 0;">a simple cupping score and physical quality values.
<li style="margin:0.38rem 0;">physical quality values and links to cupping sessions.
</li>
</ul>
Coffee compatibility is disabled by default and must be enabled on
`purchase_trade.configuration` with the `Active coffee compatibility` field.
While the option is disabled, coffee menus and coffee quality pages on lines
must not be visible.
Cupping results are entered through sessions:
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
<li style="margin:0.38rem 0;">cupping properties/criteria are configurable;
</li>
<li style="margin:0.38rem 0;">a session defines the room cup capacity and the number of cups per sample;
</li>
<li style="margin:0.38rem 0;">each sample added to the session generates several blind-coded cups;
</li>
<li style="margin:0.38rem 0;">multiple cuppers can enter a result for each cup;
</li>
<li style="margin:0.38rem 0;">cuppers see only the origin and coffee type, not the contract or supplier;
</li>
<li style="margin:0.38rem 0;">the preparation order proposes arabicas first, blends/other coffees next and robustas at the end of the session;
</li>
<li style="margin:0.38rem 0;">the final decision remains global on the session sample: approved, rejected or re-cup.
</li>
</ul>
@@ -39,6 +63,20 @@ defect count and minimum cup score.
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
<li style="margin:0.38rem 0;"><code>coffee.sample</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.criterion</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.session</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.session.sample</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.cup</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.result</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.result.line</code>
</li>
<li style="margin:0.38rem 0;"><code>purchase_trade.configuration.active_coffee_compatibility</code>
</li>
<li style="margin:0.38rem 0;"><code>purchase.line.coffee_samples</code>
</li>
<li style="margin:0.38rem 0;"><code>sale.line.coffee_samples</code>
@@ -55,6 +93,8 @@ defect count and minimum cup score.
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/coffee_sample_*.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/coffee_cupping_*.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/purchase_line_form.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/sale_line_form.xml</code>
@@ -67,7 +107,9 @@ defect count and minimum cup score.
</li>
<li style="margin:0.38rem 0;">phase 1 does not block shipment or contract validation;
</li>
<li style="margin:0.38rem 0;">lab delegation is a tracking field, not a complete lab workflow yet.
<li style="margin:0.38rem 0;">lab delegation is a tracking field, not a complete lab workflow yet;
</li>
<li style="margin:0.38rem 0;">coffee menus are inactive by default and synchronized when the configuration is saved.
</li>
</ul>
</li>

View File

@@ -25,7 +25,31 @@ Chaque echantillon porte:
</li>
<li style="margin:0.38rem 0;">une date d&#x27;expiration calculee depuis la date de demande/reception et la duree de vie de l&#x27;echantillon;
</li>
<li style="margin:0.38rem 0;">une notation de cupping simple et des donnees de qualite physique.
<li style="margin:0.38rem 0;">des donnees de qualite physique et des liens vers les seances de cupping.
</li>
</ul>
La compatibilite cafe est inactive par defaut et doit etre activee dans la
configuration `purchase_trade.configuration` avec le champ `Active coffee
compatibility`. Tant que l'option est inactive, les menus cafe et les pages
qualite cafe des lignes ne doivent pas etre visibles.
Les resultats de cupping sont saisis dans des seances:
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
<li style="margin:0.38rem 0;">les proprietes/criteres de cupping sont parametrables;
</li>
<li style="margin:0.38rem 0;">une seance definit une capacite de salle en nombre de tasses et un nombre de tasses par echantillon;
</li>
<li style="margin:0.38rem 0;">chaque echantillon inscrit a la seance genere plusieurs tasses avec un code aveugle;
</li>
<li style="margin:0.38rem 0;">plusieurs personnes peuvent saisir un resultat par tasse;
</li>
<li style="margin:0.38rem 0;">les degustateurs voient uniquement l&#x27;origine et le type de cafe, pas le contrat ni le fournisseur;
</li>
<li style="margin:0.38rem 0;">l&#x27;ordre de preparation propose les arabicas avant les blends/autres cafes et garde les robustas en fin de seance;
</li>
<li style="margin:0.38rem 0;">la decision finale reste globale a l&#x27;echantillon de seance: approved, rejected ou re-cup.
</li>
</ul>
@@ -40,6 +64,20 @@ maximum, nombre de defauts maximum et score cup minimum.
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
<li style="margin:0.38rem 0;"><code>coffee.sample</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.criterion</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.session</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.session.sample</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.cup</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.result</code>
</li>
<li style="margin:0.38rem 0;"><code>coffee.cupping.result.line</code>
</li>
<li style="margin:0.38rem 0;"><code>purchase_trade.configuration.active_coffee_compatibility</code>
</li>
<li style="margin:0.38rem 0;"><code>purchase.line.coffee_samples</code>
</li>
<li style="margin:0.38rem 0;"><code>sale.line.coffee_samples</code>
@@ -56,6 +94,8 @@ maximum, nombre de defauts maximum et score cup minimum.
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/coffee_sample_*.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/coffee_cupping_*.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/purchase_line_form.xml</code>
</li>
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/sale_line_form.xml</code>
@@ -68,7 +108,9 @@ maximum, nombre de defauts maximum et score cup minimum.
</li>
<li style="margin:0.38rem 0;">aucun blocage shipment/validation n&#x27;est introduit en phase 1;
</li>
<li style="margin:0.38rem 0;">la delegation labo reste une information de suivi, sans workflow labo complet.
<li style="margin:0.38rem 0;">la delegation labo reste une information de suivi, sans workflow labo complet;
</li>
<li style="margin:0.38rem 0;">les menus cafe sont inactifs par defaut et synchronises quand la configuration est sauvegardee.
</li>
</ul>
</li>

View File

@@ -20,7 +20,24 @@ Each sample stores:
rejected, expired, archived;
- an expiry date computed from the request/receipt date and the sample shelf
life;
- a simple cupping score and physical quality values.
- physical quality values and links to cupping sessions.
Coffee compatibility is disabled by default and must be enabled on
`purchase_trade.configuration` with the `Active coffee compatibility` field.
While the option is disabled, coffee menus and coffee quality pages on lines
must not be visible.
Cupping results are entered through sessions:
- cupping properties/criteria are configurable;
- a session defines the room cup capacity and the number of cups per sample;
- each sample added to the session generates several blind-coded cups;
- multiple cuppers can enter a result for each cup;
- cuppers see only the origin and coffee type, not the contract or supplier;
- the preparation order proposes arabicas first, blends/other coffees next and
robustas at the end of the session;
- the final decision remains global on the session sample: approved, rejected
or re-cup.
`purchase.line` and `sale.line` store the target coffee quality specifications:
origin, process, variety, crop year, screen size, maximum moisture, maximum
@@ -30,6 +47,13 @@ defect count and minimum cup score.
- Models/fields:
- `coffee.sample`
- `coffee.cupping.criterion`
- `coffee.cupping.session`
- `coffee.cupping.session.sample`
- `coffee.cupping.cup`
- `coffee.cupping.result`
- `coffee.cupping.result.line`
- `purchase_trade.configuration.active_coffee_compatibility`
- `purchase.line.coffee_samples`
- `sale.line.coffee_samples`
- `coffee_*` fields on `purchase.line` and `sale.line`
@@ -37,9 +61,12 @@ defect count and minimum cup score.
- `modules/purchase_trade/coffee.py`
- `modules/purchase_trade/coffee.xml`
- `modules/purchase_trade/view/coffee_sample_*.xml`
- `modules/purchase_trade/view/coffee_cupping_*.xml`
- `modules/purchase_trade/view/purchase_line_form.xml`
- `modules/purchase_trade/view/sale_line_form.xml`
- Caveats:
- expiry is monitored through the `Expired` and `Expiring soon` domains;
- phase 1 does not block shipment or contract validation;
- lab delegation is a tracking field, not a complete lab workflow yet.
- 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.

View File

@@ -21,7 +21,27 @@ Chaque echantillon porte:
rejected, expired, archived;
- une date d'expiration calculee depuis la date de demande/reception et la
duree de vie de l'echantillon;
- une notation de cupping simple et des donnees de qualite physique.
- des donnees de qualite physique et des liens vers les seances de cupping.
La compatibilite cafe est inactive par defaut et doit etre activee dans la
configuration `purchase_trade.configuration` avec le champ `Active coffee
compatibility`. Tant que l'option est inactive, les menus cafe et les pages
qualite cafe des lignes ne doivent pas etre visibles.
Les resultats de cupping sont saisis dans des seances:
- les proprietes/criteres de cupping sont parametrables;
- une seance definit une capacite de salle en nombre de tasses et un nombre de
tasses par echantillon;
- chaque echantillon inscrit a la seance genere plusieurs tasses avec un code
aveugle;
- plusieurs personnes peuvent saisir un resultat par tasse;
- les degustateurs voient uniquement l'origine et le type de cafe, pas le
contrat ni le fournisseur;
- l'ordre de preparation propose les arabicas avant les blends/autres cafes et
garde les robustas en fin de seance;
- la decision finale reste globale a l'echantillon de seance: approved,
rejected ou re-cup.
Les lignes `purchase.line` et `sale.line` portent les specifications qualite
cibles du cafe: origine, process, variete, crop year, screen size, humidite
@@ -31,6 +51,13 @@ maximum, nombre de defauts maximum et score cup minimum.
- Modeles/champs:
- `coffee.sample`
- `coffee.cupping.criterion`
- `coffee.cupping.session`
- `coffee.cupping.session.sample`
- `coffee.cupping.cup`
- `coffee.cupping.result`
- `coffee.cupping.result.line`
- `purchase_trade.configuration.active_coffee_compatibility`
- `purchase.line.coffee_samples`
- `sale.line.coffee_samples`
- champs `coffee_*` sur `purchase.line` et `sale.line`
@@ -38,10 +65,13 @@ maximum, nombre de defauts maximum et score cup minimum.
- `modules/purchase_trade/coffee.py`
- `modules/purchase_trade/coffee.xml`
- `modules/purchase_trade/view/coffee_sample_*.xml`
- `modules/purchase_trade/view/coffee_cupping_*.xml`
- `modules/purchase_trade/view/purchase_line_form.xml`
- `modules/purchase_trade/view/sale_line_form.xml`
- Points de vigilance:
- l'expiration est pilotee par les domaines `Expired` et `Expiring soon`;
- aucun blocage shipment/validation n'est introduit en phase 1;
- la delegation labo reste une information de suivi, sans workflow labo
complet.
complet;
- les menus cafe sont inactifs par defaut et synchronises quand la
configuration est sauvegardee.

View File

@@ -31,20 +31,53 @@ class PurchaseTradeTestCase(ModuleTestCase):
'Test purchase_trade module'
module = 'purchase_trade'
def test_coffee_sample_cup_score_sums_cupping_criteria(self):
'coffee sample cup score sums criteria and subtracts defects'
score_fields = [
'aroma', 'flavor', 'aftertaste', 'acidity', 'body', 'balance',
'sweetness', 'clean_cup', 'uniformity', 'overall']
sample = SimpleNamespace(
defects=Decimal('2.00'),
_score_fields=lambda: score_fields)
for field_name in score_fields:
setattr(sample, field_name, Decimal('8.00'))
def test_coffee_cupping_result_score_sums_configured_criteria(self):
'coffee cupping result score sums criteria and subtracts defects'
score = SimpleNamespace(subtract_from_score=False)
defect = SimpleNamespace(subtract_from_score=True)
result = SimpleNamespace(lines=[
SimpleNamespace(score=Decimal('8.00'), criterion=score),
SimpleNamespace(score=Decimal('8.25'), criterion=score),
SimpleNamespace(score=Decimal('2.00'), criterion=defect),
])
self.assertEqual(
coffee_module.CoffeeSample.get_total_score(sample),
Decimal('78.00'))
coffee_module.CoffeeCuppingResult.get_total_score(result),
Decimal('14.25'))
def test_coffee_cupping_session_counts_requested_cups(self):
'coffee cupping session capacity is based on sample cups'
session = SimpleNamespace(cups_per_sample=3)
lines = [
SimpleNamespace(cups_per_sample=None, session=session),
SimpleNamespace(cups_per_sample=4, session=session),
]
self.assertEqual(
coffee_module.CoffeeCuppingSession._total_requested_cups(lines),
7)
def test_coffee_cupping_session_orders_arabica_before_robusta(self):
'coffee cupping session tasting order keeps robusta last'
arabica = SimpleNamespace(
id=2,
reference='A',
get_public_origin=lambda: 'Colombia',
get_public_coffee_type=lambda: 'arabica')
robusta = SimpleNamespace(
id=1,
reference='R',
get_public_origin=lambda: 'Vietnam',
get_public_coffee_type=lambda: 'robusta')
lines = [
SimpleNamespace(sample=robusta),
SimpleNamespace(sample=arabica),
]
ordered = sorted(
lines, key=coffee_module.CoffeeCuppingSession._line_sort_key)
self.assertEqual([line.sample.reference for line in ordered], ['A', 'R'])
def test_coffee_sample_default_expiry_date_uses_shelf_life(self):
'coffee sample expiry date defaults from request date and shelf life'

View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<form col="4">
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
<label name="sequence"/>
<field name="sequence"/>
<label name="kind"/>
<field name="kind"/>
<label name="max_score"/>
<field name="max_score"/>
<label name="subtract_from_score"/>
<field name="subtract_from_score"/>
<newline/>
<field name="description" colspan="4"/>
</form>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<tree sequence="sequence">
<field name="sequence"/>
<field name="name"/>
<field name="kind"/>
<field name="max_score"/>
<field name="subtract_from_score"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<form col="4">
<label name="blind_code"/>
<field name="blind_code"/>
<label name="cup_no"/>
<field name="cup_no"/>
<label name="public_origin"/>
<field name="public_origin"/>
<label name="public_coffee_type"/>
<field name="public_coffee_type"/>
<label name="average_score"/>
<field name="average_score"/>
<label name="defect_count"/>
<field name="defect_count"/>
<newline/>
<field name="results" colspan="4" mode="tree,form"
view_ids="purchase_trade.coffee_cupping_result_view_tree,purchase_trade.coffee_cupping_result_view_form"/>
</form>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<tree>
<field name="blind_code"/>
<field name="cup_no"/>
<field name="public_origin"/>
<field name="public_coffee_type"/>
<field name="average_score"/>
<field name="defect_count"/>
</tree>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<form col="4">
<label name="cup"/>
<field name="cup"/>
<label name="cupper"/>
<field name="cupper"/>
<label name="date"/>
<field name="date"/>
<label name="has_defect"/>
<field name="has_defect"/>
<label name="total_score"/>
<field name="total_score"/>
<newline/>
<field name="lines" colspan="4" mode="tree"
view_ids="purchase_trade.coffee_cupping_result_line_view_tree"/>
<newline/>
<field name="notes" colspan="4"/>
</form>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<tree>
<field name="criterion"/>
<field name="score"/>
<field name="defect_found"/>
<field name="notes"/>
</tree>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<tree>
<field name="cup"/>
<field name="cupper"/>
<field name="date"/>
<field name="has_defect"/>
<field name="total_score"/>
</tree>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<form col="4">
<label name="name"/>
<field name="name"/>
<label name="state"/>
<field name="state"/>
<newline/>
<button name="prepare" string="Prepare"/>
<button name="start" string="Start"/>
<button name="done" string="Done"/>
<button name="cancel" string="Cancel"/>
<button name="draft" string="Draft"/>
<newline/>
<label name="date"/>
<field name="date"/>
<label name="responsible"/>
<field name="responsible"/>
<label name="location"/>
<field name="location"/>
<label name="cup_capacity"/>
<field name="cup_capacity"/>
<label name="cups_per_sample"/>
<field name="cups_per_sample"/>
<label name="total_cups"/>
<field name="total_cups"/>
<label name="remaining_cups"/>
<field name="remaining_cups"/>
<newline/>
<field name="lines" colspan="4" mode="tree,form"
view_ids="purchase_trade.coffee_cupping_session_sample_view_tree,purchase_trade.coffee_cupping_session_sample_view_form"/>
</form>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<form col="4">
<label name="sequence"/>
<field name="sequence"/>
<label name="sample"/>
<field name="sample"/>
<label name="public_origin"/>
<field name="public_origin"/>
<label name="public_coffee_type"/>
<field name="public_coffee_type"/>
<label name="cups_per_sample"/>
<field name="cups_per_sample"/>
<label name="result_count"/>
<field name="result_count"/>
<label name="average_score"/>
<field name="average_score"/>
<label name="defect_count"/>
<field name="defect_count"/>
<label name="decision"/>
<field name="decision"/>
<newline/>
<field name="cups" colspan="4" mode="tree,form"
view_ids="purchase_trade.coffee_cupping_cup_view_tree,purchase_trade.coffee_cupping_cup_view_form"/>
<newline/>
<field name="decision_note" colspan="4"/>
</form>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<tree sequence="sequence">
<field name="sequence"/>
<field name="sample"/>
<field name="public_origin"/>
<field name="public_coffee_type"/>
<field name="cups_per_sample"/>
<field name="result_count"/>
<field name="average_score"/>
<field name="defect_count"/>
<field name="decision"/>
</tree>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<tree>
<field name="date"/>
<field name="name"/>
<field name="state"/>
<field name="responsible"/>
<field name="location"/>
<field name="cup_capacity"/>
<field name="total_cups"/>
<field name="remaining_cups"/>
</tree>

View File

@@ -28,6 +28,10 @@
<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"/>
@@ -61,32 +65,6 @@
<label name="lab_due_date"/>
<field name="lab_due_date"/>
<newline/>
<separator string="Cupping" id="cupping" colspan="4"/>
<label name="aroma"/>
<field name="aroma"/>
<label name="flavor"/>
<field name="flavor"/>
<label name="aftertaste"/>
<field name="aftertaste"/>
<label name="acidity"/>
<field name="acidity"/>
<label name="body"/>
<field name="body"/>
<label name="balance"/>
<field name="balance"/>
<label name="sweetness"/>
<field name="sweetness"/>
<label name="clean_cup"/>
<field name="clean_cup"/>
<label name="uniformity"/>
<field name="uniformity"/>
<label name="overall"/>
<field name="overall"/>
<label name="defects"/>
<field name="defects"/>
<label name="total_score"/>
<field name="total_score"/>
<newline/>
<separator string="Physical quality" id="physical_quality" colspan="4"/>
<label name="moisture"/>
<field name="moisture"/>
@@ -95,6 +73,10 @@
<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"/>

View File

@@ -6,8 +6,9 @@
<field name="state"/>
<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="total_score"/>
<field name="lab"/>
</tree>

View File

@@ -2,4 +2,6 @@
<form col="4">
<label name="pricing_rule"/>
<field name="pricing_rule" colspan="3"/>
<label name="active_coffee_compatibility"/>
<field name="active_coffee_compatibility"/>
</form>

View File

@@ -134,11 +134,15 @@ this repository contains the full copyright notices and license terms. -->
</page>
</xpath>
<xpath expr="/form/notebook/page[@id='general']" position="after">
<page string="Coffee Quality" col="4" id="coffee_quality">
<page string="Coffee Quality" col="4" id="coffee_quality"
states="{'invisible': ~Eval('active_coffee_compatibility')}">
<field name="active_coffee_compatibility" invisible="1"/>
<label name="coffee_quality_status"/>
<field name="coffee_quality_status"/>
<label name="coffee_origin"/>
<field name="coffee_origin"/>
<label name="coffee_type"/>
<field name="coffee_type"/>
<label name="coffee_process"/>
<field name="coffee_process"/>
<label name="coffee_variety"/>

View File

@@ -127,11 +127,15 @@ this repository contains the full copyright notices and license terms. -->
</page>
</xpath>
<xpath expr="/form/notebook/page[@id='general']" position="after">
<page string="Coffee Quality" col="4" id="coffee_quality">
<page string="Coffee Quality" col="4" id="coffee_quality"
states="{'invisible': ~Eval('active_coffee_compatibility')}">
<field name="active_coffee_compatibility" invisible="1"/>
<label name="coffee_quality_status"/>
<field name="coffee_quality_status"/>
<label name="coffee_origin"/>
<field name="coffee_origin"/>
<label name="coffee_type"/>
<field name="coffee_type"/>
<label name="coffee_process"/>
<field name="coffee_process"/>
<label name="coffee_variety"/>