Coffee
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,7 +25,31 @@ Chaque echantillon porte:
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">une date d'expiration calculee depuis la date de demande/reception et la duree de vie de l'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'origine et le type de cafe, pas le contrat ni le fournisseur;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">l'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'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'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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
18
modules/purchase_trade/view/coffee_cupping_cup_form.xml
Normal file
18
modules/purchase_trade/view/coffee_cupping_cup_form.xml
Normal 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>
|
||||
9
modules/purchase_trade/view/coffee_cupping_cup_tree.xml
Normal file
9
modules/purchase_trade/view/coffee_cupping_cup_tree.xml
Normal 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>
|
||||
18
modules/purchase_trade/view/coffee_cupping_result_form.xml
Normal file
18
modules/purchase_trade/view/coffee_cupping_result_form.xml
Normal 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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<tree>
|
||||
<field name="criterion"/>
|
||||
<field name="score"/>
|
||||
<field name="defect_found"/>
|
||||
<field name="notes"/>
|
||||
</tree>
|
||||
@@ -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>
|
||||
31
modules/purchase_trade/view/coffee_cupping_session_form.xml
Normal file
31
modules/purchase_trade/view/coffee_cupping_session_form.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
modules/purchase_trade/view/coffee_cupping_session_tree.xml
Normal file
11
modules/purchase_trade/view/coffee_cupping_session_tree.xml
Normal 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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user