Coffee
This commit is contained in:
@@ -37,6 +37,7 @@ from . import (
|
||||
service,
|
||||
invoice,
|
||||
duplicate,
|
||||
coffee,
|
||||
)
|
||||
|
||||
def register():
|
||||
@@ -94,6 +95,7 @@ def register():
|
||||
configuration.Configuration,
|
||||
pricing.ImportPricesStart,
|
||||
pricing.ImportPricesResult,
|
||||
coffee.CoffeeSample,
|
||||
duplicate.TradeCustomDuplicateStart,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
@@ -239,10 +241,11 @@ def register():
|
||||
purchase.AssayUnit,
|
||||
purchase.PayableRule,
|
||||
purchase.PenaltyRule,
|
||||
purchase.PenaltyRuleTier,
|
||||
purchase.ConcentrateTerm,
|
||||
backtoback.Backtoback,
|
||||
dimension.AnalyticDimension,
|
||||
purchase.PenaltyRuleTier,
|
||||
purchase.ConcentrateTerm,
|
||||
coffee.PurchaseLine,
|
||||
backtoback.Backtoback,
|
||||
dimension.AnalyticDimension,
|
||||
dimension.AnalyticDimensionValue,
|
||||
dimension.AnalyticDimensionAssignment,
|
||||
weight_report.WeightReport,
|
||||
@@ -306,9 +309,10 @@ def register():
|
||||
sale.OpenPosition,
|
||||
sale.Backtoback,
|
||||
sale.AnalyticDimensionAssignment,
|
||||
sale.PriceComposition,
|
||||
sale.PremiumComposition,
|
||||
module='sale', type_='model')
|
||||
sale.PriceComposition,
|
||||
sale.PremiumComposition,
|
||||
coffee.SaleLine,
|
||||
module='sale', type_='model')
|
||||
Pool.register(
|
||||
lot.LotShipping,
|
||||
lot.LotPlanTransport,
|
||||
|
||||
429
modules/purchase_trade/coffee.py
Normal file
429
modules/purchase_trade/coffee.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from trytond.model import ModelSQL, ModelView, Workflow, fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
|
||||
|
||||
COFFEE_SAMPLE_TYPES = [
|
||||
(None, ''),
|
||||
('offer', 'Offer sample'),
|
||||
('pre_shipment', 'Pre-shipment sample'),
|
||||
('shipment', 'Shipment sample'),
|
||||
('arrival', 'Arrival sample'),
|
||||
('stock', 'Stock sample'),
|
||||
('customer', 'Customer sample'),
|
||||
('retained', 'Retained sample'),
|
||||
]
|
||||
|
||||
COFFEE_PROCESS_TYPES = [
|
||||
(None, ''),
|
||||
('washed', 'Washed'),
|
||||
('natural', 'Natural'),
|
||||
('honey', 'Honey'),
|
||||
('semi_washed', 'Semi-washed'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
|
||||
class CoffeeSample(ModelSQL, ModelView, Workflow):
|
||||
'Coffee Sample'
|
||||
__name__ = 'coffee.sample'
|
||||
|
||||
reference = fields.Char('Reference')
|
||||
direction = fields.Selection([
|
||||
(None, ''),
|
||||
('purchase', 'Purchase'),
|
||||
('sale', 'Sale'),
|
||||
], 'Direction', required=True)
|
||||
sample_type = fields.Selection(
|
||||
COFFEE_SAMPLE_TYPES, 'Sample Type', required=True)
|
||||
state = fields.Selection([
|
||||
(None, ''),
|
||||
('requested', 'Requested'),
|
||||
('received', 'Received'),
|
||||
('sent_to_lab', 'Sent to lab'),
|
||||
('under_review', 'Under review'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
('archived', 'Archived'),
|
||||
], 'State', readonly=True, required=True)
|
||||
|
||||
purchase_line = fields.Many2One(
|
||||
'purchase.line', 'Purchase Line', ondelete='CASCADE',
|
||||
states={'invisible': Eval('direction') != 'purchase'},
|
||||
depends=['direction'])
|
||||
sale_line = fields.Many2One(
|
||||
'sale.line', 'Sale Line', ondelete='CASCADE',
|
||||
states={'invisible': Eval('direction') != 'sale'},
|
||||
depends=['direction'])
|
||||
lot = fields.Many2One('lot.lot', 'Lot')
|
||||
product = fields.Function(
|
||||
fields.Many2One('product.product', 'Product'), 'get_product')
|
||||
party = fields.Function(
|
||||
fields.Many2One('party.party', 'Party'), 'get_party')
|
||||
|
||||
requested_date = fields.Date('Requested Date')
|
||||
received_date = fields.Date('Received Date')
|
||||
sent_date = fields.Date('Sent Date')
|
||||
review_date = fields.Date('Review Date')
|
||||
decision_date = fields.Date('Decision Date')
|
||||
shelf_life_days = fields.Integer('Shelf life (days)')
|
||||
expiry_date = fields.Date('Expiry Date')
|
||||
is_expired = fields.Function(
|
||||
fields.Boolean('Expired'), 'get_is_expired',
|
||||
searcher='search_is_expired')
|
||||
expires_soon = fields.Function(
|
||||
fields.Boolean('Expires soon'), 'get_expires_soon',
|
||||
searcher='search_expires_soon')
|
||||
|
||||
quantity = fields.Numeric('Quantity', digits='unit')
|
||||
unit = fields.Many2One('product.uom', 'Unit')
|
||||
storage_location = fields.Char('Sample location')
|
||||
delegated_to_lab = fields.Boolean('Delegated to lab')
|
||||
lab = fields.Many2One(
|
||||
'party.party', 'Lab',
|
||||
states={'invisible': ~Eval('delegated_to_lab')},
|
||||
depends=['delegated_to_lab'])
|
||||
lab_due_date = fields.Date(
|
||||
'Lab due date',
|
||||
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')
|
||||
notes = fields.Text('Notes')
|
||||
decision_note = fields.Text('Decision note')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._transitions |= set((
|
||||
('requested', 'received'),
|
||||
('received', 'sent_to_lab'),
|
||||
('received', 'under_review'),
|
||||
('sent_to_lab', 'under_review'),
|
||||
('under_review', 'approved'),
|
||||
('under_review', 'rejected'),
|
||||
('requested', 'expired'),
|
||||
('received', 'expired'),
|
||||
('sent_to_lab', 'expired'),
|
||||
('under_review', 'expired'),
|
||||
('approved', 'archived'),
|
||||
('rejected', 'archived'),
|
||||
('expired', 'archived'),
|
||||
('archived', 'requested'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'receive': {
|
||||
'invisible': Eval('state') != 'requested',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'send_to_lab': {
|
||||
'invisible': ~Eval('state').in_(['received']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'review': {
|
||||
'invisible': ~Eval('state').in_(['received', 'sent_to_lab']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'approve': {
|
||||
'invisible': Eval('state') != 'under_review',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'reject': {
|
||||
'invisible': Eval('state') != 'under_review',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'expire': {
|
||||
'invisible': Eval('state').in_(
|
||||
['approved', 'rejected', 'expired', 'archived']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'archive': {
|
||||
'invisible': ~Eval('state').in_(
|
||||
['approved', 'rejected', 'expired']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'reset_to_requested': {
|
||||
'invisible': Eval('state') != 'archived',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def default_state():
|
||||
return 'requested'
|
||||
|
||||
@staticmethod
|
||||
def default_shelf_life_days():
|
||||
return 90
|
||||
|
||||
@staticmethod
|
||||
def default_requested_date():
|
||||
return Pool().get('ir.date').today()
|
||||
|
||||
@classmethod
|
||||
def default_direction(cls):
|
||||
return 'purchase'
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
for values in vlist:
|
||||
cls._set_direction_from_line(values)
|
||||
cls._set_default_expiry_date(values, force=True)
|
||||
return super().create(vlist)
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
actions = iter(args)
|
||||
args = []
|
||||
for records, values in zip(actions, actions):
|
||||
values = values.copy()
|
||||
cls._set_direction_from_line(values)
|
||||
if (
|
||||
{'requested_date', 'received_date', 'shelf_life_days'}
|
||||
& set(values)
|
||||
and not any(
|
||||
getattr(record, 'expiry_date', None)
|
||||
for record in records)):
|
||||
cls._set_default_expiry_date(values)
|
||||
args.extend((records, values))
|
||||
super().write(*args)
|
||||
|
||||
@classmethod
|
||||
def _set_direction_from_line(cls, values):
|
||||
if values.get('purchase_line') and not values.get('direction'):
|
||||
values['direction'] = 'purchase'
|
||||
elif values.get('sale_line') and not values.get('direction'):
|
||||
values['direction'] = 'sale'
|
||||
|
||||
@classmethod
|
||||
def _set_default_expiry_date(cls, values, force=False):
|
||||
if values.get('expiry_date'):
|
||||
return
|
||||
if not force and not (
|
||||
{'requested_date', 'received_date', 'shelf_life_days'}
|
||||
& set(values)):
|
||||
return
|
||||
start = (
|
||||
values.get('received_date')
|
||||
or values.get('requested_date')
|
||||
or cls.default_requested_date())
|
||||
shelf_life_days = values.get('shelf_life_days')
|
||||
if shelf_life_days is None:
|
||||
shelf_life_days = cls.default_shelf_life_days()
|
||||
if start and shelf_life_days:
|
||||
values['expiry_date'] = (
|
||||
start + datetime.timedelta(days=int(shelf_life_days)))
|
||||
|
||||
def get_product(self, name=None):
|
||||
line = self.purchase_line or self.sale_line
|
||||
product = getattr(line, 'product', None)
|
||||
return product.id if product else None
|
||||
|
||||
def get_party(self, name=None):
|
||||
if self.purchase_line and self.purchase_line.purchase:
|
||||
party = self.purchase_line.purchase.party
|
||||
return party.id if party else None
|
||||
if self.sale_line and self.sale_line.sale:
|
||||
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_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_is_expired(self, name=None):
|
||||
if self.state == 'expired':
|
||||
return True
|
||||
today = Pool().get('ir.date').today()
|
||||
return bool(
|
||||
self.expiry_date and self.expiry_date < today
|
||||
and self.state not in {'archived', 'rejected'})
|
||||
|
||||
def get_expires_soon(self, name=None):
|
||||
today = Pool().get('ir.date').today()
|
||||
limit = today + datetime.timedelta(days=14)
|
||||
return bool(
|
||||
self.expiry_date and today <= self.expiry_date <= limit
|
||||
and self.state not in {'archived', 'expired', 'rejected'})
|
||||
|
||||
@classmethod
|
||||
def search_is_expired(cls, name, clause):
|
||||
today = Pool().get('ir.date').today()
|
||||
positive = clause[2]
|
||||
domain = ['OR',
|
||||
('state', '=', 'expired'),
|
||||
[
|
||||
('expiry_date', '<', today),
|
||||
('state', 'not in', ['archived', 'rejected']),
|
||||
],
|
||||
]
|
||||
if positive:
|
||||
return domain
|
||||
return ['NOT', domain]
|
||||
|
||||
@classmethod
|
||||
def search_expires_soon(cls, name, clause):
|
||||
today = Pool().get('ir.date').today()
|
||||
limit = today + datetime.timedelta(days=14)
|
||||
domain = [
|
||||
('expiry_date', '>=', today),
|
||||
('expiry_date', '<=', limit),
|
||||
('state', 'not in', ['archived', 'expired', 'rejected']),
|
||||
]
|
||||
if clause[2]:
|
||||
return domain
|
||||
return ['NOT', domain]
|
||||
|
||||
def get_rec_name(self, name):
|
||||
if self.reference:
|
||||
return self.reference
|
||||
label = self.sample_type or 'sample'
|
||||
if self.purchase_line and self.purchase_line.purchase:
|
||||
return '%s - %s' % (label, self.purchase_line.purchase.rec_name)
|
||||
if self.sale_line and self.sale_line.sale:
|
||||
return '%s - %s' % (label, self.sale_line.sale.rec_name)
|
||||
return label
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('received')
|
||||
def receive(cls, samples):
|
||||
today = Pool().get('ir.date').today()
|
||||
cls.write(
|
||||
[sample for sample in samples if not sample.received_date],
|
||||
{'received_date': today})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('sent_to_lab')
|
||||
def send_to_lab(cls, samples):
|
||||
today = Pool().get('ir.date').today()
|
||||
cls.write(samples, {
|
||||
'delegated_to_lab': True,
|
||||
'sent_date': today,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('under_review')
|
||||
def review(cls, samples):
|
||||
today = Pool().get('ir.date').today()
|
||||
cls.write(samples, {'review_date': today})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('approved')
|
||||
def approve(cls, samples):
|
||||
today = Pool().get('ir.date').today()
|
||||
cls.write(samples, {'decision_date': today})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('rejected')
|
||||
def reject(cls, samples):
|
||||
today = Pool().get('ir.date').today()
|
||||
cls.write(samples, {'decision_date': today})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('expired')
|
||||
def expire(cls, samples):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('archived')
|
||||
def archive(cls, samples):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('requested')
|
||||
def reset_to_requested(cls, samples):
|
||||
pass
|
||||
|
||||
|
||||
class CoffeeLineMixin:
|
||||
coffee_samples = fields.One2Many(
|
||||
'coffee.sample', None, 'Coffee samples')
|
||||
coffee_quality_status = fields.Function(fields.Selection([
|
||||
(None, ''),
|
||||
('none', 'No sample'),
|
||||
('pending', 'Pending'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
], 'Coffee quality'), 'get_coffee_quality_status')
|
||||
coffee_origin = fields.Char('Coffee origin')
|
||||
coffee_process = fields.Selection(COFFEE_PROCESS_TYPES, 'Coffee process')
|
||||
coffee_variety = fields.Char('Coffee variety')
|
||||
coffee_crop_year = fields.Char('Crop year')
|
||||
coffee_screen_size = fields.Char('Screen size')
|
||||
coffee_moisture_max = fields.Numeric('Moisture max %', digits=(5, 2))
|
||||
coffee_defect_max = fields.Integer('Defects max')
|
||||
coffee_cup_score_min = fields.Numeric(
|
||||
'Cup score min', digits=(5, 2))
|
||||
|
||||
def _coffee_samples(self):
|
||||
return list(getattr(self, 'coffee_samples', None) or [])
|
||||
|
||||
def get_coffee_quality_status(self, name=None):
|
||||
samples = self._coffee_samples()
|
||||
if not samples:
|
||||
return 'none'
|
||||
if any(sample.state == 'rejected' for sample in samples):
|
||||
return 'rejected'
|
||||
if any(sample.state == 'expired' or sample.get_is_expired()
|
||||
for sample in samples):
|
||||
return 'expired'
|
||||
if any(sample.state == 'approved' for sample in samples):
|
||||
return 'approved'
|
||||
return 'pending'
|
||||
|
||||
|
||||
class PurchaseLine(CoffeeLineMixin, metaclass=PoolMeta):
|
||||
__name__ = 'purchase.line'
|
||||
|
||||
coffee_samples = fields.One2Many(
|
||||
'coffee.sample', 'purchase_line', 'Coffee samples')
|
||||
|
||||
|
||||
class SaleLine(CoffeeLineMixin, metaclass=PoolMeta):
|
||||
__name__ = 'sale.line'
|
||||
|
||||
coffee_samples = fields.One2Many(
|
||||
'coffee.sample', 'sale_line', 'Coffee samples')
|
||||
136
modules/purchase_trade/coffee.xml
Normal file
136
modules/purchase_trade/coffee.xml
Normal file
@@ -0,0 +1,136 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="coffee_sample_view_tree">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">coffee_sample_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="coffee_sample_view_form">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">coffee_sample_form</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"/>
|
||||
<field name="view" ref="coffee_sample_view_tree"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_coffee_sample_form_view_form">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="coffee_sample_view_form"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_coffee_sample_domain_pending">
|
||||
<field name="name">Pending</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="domain" eval="[('state', 'in', ['requested', 'received', 'sent_to_lab', 'under_review'])]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_coffee_sample_domain_expiring">
|
||||
<field name="name">Expiring soon</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="domain" eval="[('expires_soon', '=', True)]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_coffee_sample_domain_expired">
|
||||
<field name="name">Expired</field>
|
||||
<field name="sequence" eval="30"/>
|
||||
<field name="domain" eval="[('is_expired', '=', True)]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_coffee_sample_domain_approved">
|
||||
<field name="name">Approved</field>
|
||||
<field name="sequence" eval="40"/>
|
||||
<field name="domain" eval="[('state', '=', 'approved')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_coffee_sample_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_coffee_sample_domain_all">
|
||||
<field name="name">All</field>
|
||||
<field name="sequence" eval="9999"/>
|
||||
<field name="domain"></field>
|
||||
<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.model.button" id="coffee_sample_receive_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">receive</field>
|
||||
<field name="string">Receive</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_send_to_lab_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">send_to_lab</field>
|
||||
<field name="string">Send to lab</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_review_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">review</field>
|
||||
<field name="string">Review</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_approve_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">approve</field>
|
||||
<field name="string">Approve</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_reject_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">reject</field>
|
||||
<field name="string">Reject</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_expire_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">expire</field>
|
||||
<field name="string">Expire</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_archive_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">archive</field>
|
||||
<field name="string">Archive</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="coffee_sample_reset_to_requested_button">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="name">reset_to_requested</field>
|
||||
<field name="string">Reset</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_coffee_sample">
|
||||
<field name="model">coffee.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_sample_purchase">
|
||||
<field name="model">coffee.sample</field>
|
||||
<field name="group" ref="purchase.group_purchase"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -17,6 +17,8 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Pricing manuel, basis, premium, linked currency: <a href="pricing.md">pricing.md</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Trading cafe, echantillons, cupping, qualite: <a href="coffee.md">FR</a> / <a href="coffee.en.md">EN</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fees, freight, lots effectifs, quantités, <code>% rate</code>: <a href="fees.md">FR</a> / <a href="fees.en.md">EN</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Valuation, PnL, MTM, derivatives: <a href="valuation-pnl-mtm.md">FR</a> / <a href="valuation-pnl-mtm.en.md">EN</a>
|
||||
@@ -66,6 +68,8 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-PRI-003</code>: pricing manuel.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-COF-001</code>: echantillons cafe phase 1.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-001</code>: maritime freight depuis fee shipment.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-002</code>: lots effectifs des fees.
|
||||
|
||||
74
modules/purchase_trade/docs/business/coffee.en.md
Normal file
74
modules/purchase_trade/docs/business/coffee.en.md
Normal file
@@ -0,0 +1,74 @@
|
||||
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||
|
||||
# Coffee trading - samples and quality
|
||||
|
||||
Mirror page: [coffee.md](coffee.md)
|
||||
|
||||
## BR-PT-COF-001 - Coffee sample phase 1
|
||||
|
||||
Status: active
|
||||
Source: project decision on 2026-06-29
|
||||
|
||||
### Business rule
|
||||
|
||||
Coffee trading must track samples linked to purchase and sale lines.
|
||||
|
||||
Each sample stores:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">a sample type: offer, pre-shipment, shipment, arrival, stock, customer or retained;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">a purchase or sale direction;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">a lifecycle: requested, received, sent to lab, under review, approved, rejected, expired, archived;
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
`purchase.line` and `sale.line` store the target coffee quality specifications:
|
||||
origin, process, variety, crop year, screen size, maximum moisture, maximum
|
||||
defect count and minimum cup score.
|
||||
|
||||
### Developer notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Models/fields:
|
||||
<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>purchase.line.coffee_samples</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>sale.line.coffee_samples</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>coffee_*</code> fields on <code>purchase.line</code> and <code>sale.line</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Files:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/coffee.py</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/coffee.xml</code>
|
||||
</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/purchase_line_form.xml</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/sale_line_form.xml</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Caveats:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">expiry is monitored through the <code>Expired</code> and <code>Expiring soon</code> domains;
|
||||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
75
modules/purchase_trade/docs/business/coffee.md
Normal file
75
modules/purchase_trade/docs/business/coffee.md
Normal file
@@ -0,0 +1,75 @@
|
||||
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||
|
||||
# Trading cafe - echantillons et qualite
|
||||
|
||||
Page miroir: [coffee.en.md](coffee.en.md)
|
||||
|
||||
## BR-PT-COF-001 - Phase 1 echantillons cafe
|
||||
|
||||
Statut: active
|
||||
Source: decision projet du 2026-06-29
|
||||
|
||||
### Regle consultant
|
||||
|
||||
Le trading cafe doit pouvoir piloter les echantillons rattaches aux lignes
|
||||
d'achat et de vente.
|
||||
|
||||
Chaque echantillon porte:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">un type d'echantillon: offer, pre-shipment, shipment, arrival, stock, customer ou retained;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">un sens purchase ou sale;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">un cycle de vie: requested, received, sent to lab, under review, approved, rejected, expired, archived;
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
Les lignes `purchase.line` et `sale.line` portent les specifications qualite
|
||||
cibles du cafe: origine, process, variete, crop year, screen size, humidite
|
||||
maximum, nombre de defauts maximum et score cup minimum.
|
||||
|
||||
### Notes developpeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Modeles/champs:
|
||||
<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>purchase.line.coffee_samples</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>sale.line.coffee_samples</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">champs <code>coffee_*</code> sur <code>purchase.line</code> et <code>sale.line</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fichiers:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/coffee.py</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/coffee.xml</code>
|
||||
</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/purchase_line_form.xml</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>modules/purchase_trade/view/sale_line_form.xml</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Points de vigilance:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">l'expiration est pilotee par les domaines <code>Expired</code> et <code>Expiring soon</code>;
|
||||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -9,6 +9,7 @@ Statut: `migration partielle`
|
||||
- Matching, Create Contracts, back-to-back: [matching.md](matching.md)
|
||||
- Shipments, controllers, SLA, weight reports: [shipments-execution.md](shipments-execution.md)
|
||||
- Pricing manuel, basis, premium, linked currency: [pricing.md](pricing.md)
|
||||
- Trading cafe, echantillons, cupping, qualite: [FR](coffee.md) / [EN](coffee.en.md)
|
||||
- Fees, freight, lots effectifs, quantités, `% rate`: [FR](fees.md) / [EN](fees.en.md)
|
||||
- Valuation, PnL, MTM, derivatives: [FR](valuation-pnl-mtm.md) / [EN](valuation-pnl-mtm.en.md)
|
||||
- Factures provisoires/finales, padding: [invoicing.md](invoicing.md)
|
||||
@@ -35,6 +36,7 @@ Statut: `migration partielle`
|
||||
- `BR-PT-PRI-001`: premium dans priced et basis.
|
||||
- `BR-PT-PRI-002`: linked currency.
|
||||
- `BR-PT-PRI-003`: pricing manuel.
|
||||
- `BR-PT-COF-001`: echantillons cafe phase 1.
|
||||
- `BR-PT-FEE-001`: maritime freight depuis fee shipment.
|
||||
- `BR-PT-FEE-002`: lots effectifs des fees.
|
||||
- `BR-PT-FEE-003`: quantité du fee alignée sur les lots effectifs.
|
||||
|
||||
45
modules/purchase_trade/docs_source/business/coffee.en.md
Normal file
45
modules/purchase_trade/docs_source/business/coffee.en.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Coffee trading - samples and quality
|
||||
|
||||
Mirror page: [coffee.md](coffee.md)
|
||||
|
||||
## BR-PT-COF-001 - Coffee sample phase 1
|
||||
|
||||
Status: active
|
||||
Source: project decision on 2026-06-29
|
||||
|
||||
### Business rule
|
||||
|
||||
Coffee trading must track samples linked to purchase and sale lines.
|
||||
|
||||
Each sample stores:
|
||||
|
||||
- a sample type: offer, pre-shipment, shipment, arrival, stock, customer or
|
||||
retained;
|
||||
- a purchase or sale direction;
|
||||
- a lifecycle: requested, received, sent to lab, under review, approved,
|
||||
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.
|
||||
|
||||
`purchase.line` and `sale.line` store the target coffee quality specifications:
|
||||
origin, process, variety, crop year, screen size, maximum moisture, maximum
|
||||
defect count and minimum cup score.
|
||||
|
||||
### Developer notes
|
||||
|
||||
- Models/fields:
|
||||
- `coffee.sample`
|
||||
- `purchase.line.coffee_samples`
|
||||
- `sale.line.coffee_samples`
|
||||
- `coffee_*` fields on `purchase.line` and `sale.line`
|
||||
- Files:
|
||||
- `modules/purchase_trade/coffee.py`
|
||||
- `modules/purchase_trade/coffee.xml`
|
||||
- `modules/purchase_trade/view/coffee_sample_*.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.
|
||||
47
modules/purchase_trade/docs_source/business/coffee.md
Normal file
47
modules/purchase_trade/docs_source/business/coffee.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Trading cafe - echantillons et qualite
|
||||
|
||||
Page miroir: [coffee.en.md](coffee.en.md)
|
||||
|
||||
## BR-PT-COF-001 - Phase 1 echantillons cafe
|
||||
|
||||
Statut: active
|
||||
Source: decision projet du 2026-06-29
|
||||
|
||||
### Regle consultant
|
||||
|
||||
Le trading cafe doit pouvoir piloter les echantillons rattaches aux lignes
|
||||
d'achat et de vente.
|
||||
|
||||
Chaque echantillon porte:
|
||||
|
||||
- un type d'echantillon: offer, pre-shipment, shipment, arrival, stock,
|
||||
customer ou retained;
|
||||
- un sens purchase ou sale;
|
||||
- un cycle de vie: requested, received, sent to lab, under review, approved,
|
||||
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.
|
||||
|
||||
Les lignes `purchase.line` et `sale.line` portent les specifications qualite
|
||||
cibles du cafe: origine, process, variete, crop year, screen size, humidite
|
||||
maximum, nombre de defauts maximum et score cup minimum.
|
||||
|
||||
### Notes developpeur
|
||||
|
||||
- Modeles/champs:
|
||||
- `coffee.sample`
|
||||
- `purchase.line.coffee_samples`
|
||||
- `sale.line.coffee_samples`
|
||||
- champs `coffee_*` sur `purchase.line` et `sale.line`
|
||||
- Fichiers:
|
||||
- `modules/purchase_trade/coffee.py`
|
||||
- `modules/purchase_trade/coffee.xml`
|
||||
- `modules/purchase_trade/view/coffee_sample_*.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.
|
||||
@@ -22,6 +22,7 @@ from trytond.modules.purchase_trade import stock as stock_module
|
||||
from trytond.modules.purchase_trade import fee as fee_module
|
||||
from trytond.modules.purchase_trade import pricing as pricing_module
|
||||
from trytond.modules.purchase_trade import duplicate as duplicate_module
|
||||
from trytond.modules.purchase_trade import coffee as coffee_module
|
||||
from trytond.modules.purchase_trade import company_defaults
|
||||
from trytond.modules.purchase_trade.service import ContractFactory
|
||||
|
||||
@@ -30,6 +31,45 @@ 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'))
|
||||
|
||||
self.assertEqual(
|
||||
coffee_module.CoffeeSample.get_total_score(sample),
|
||||
Decimal('78.00'))
|
||||
|
||||
def test_coffee_sample_default_expiry_date_uses_shelf_life(self):
|
||||
'coffee sample expiry date defaults from request date and shelf life'
|
||||
values = {
|
||||
'requested_date': datetime.date(2026, 6, 29),
|
||||
'shelf_life_days': 30,
|
||||
}
|
||||
|
||||
coffee_module.CoffeeSample._set_default_expiry_date(
|
||||
values, force=True)
|
||||
|
||||
self.assertEqual(
|
||||
values['expiry_date'], datetime.date(2026, 7, 29))
|
||||
|
||||
@with_transaction()
|
||||
def test_purchase_line_coffee_quality_status_uses_samples(self):
|
||||
'purchase line coffee quality status follows linked samples'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
line = PurchaseLine()
|
||||
line.coffee_samples = [
|
||||
Mock(state='approved', get_is_expired=Mock(return_value=False))]
|
||||
|
||||
self.assertEqual(
|
||||
line.get_coffee_quality_status(), 'approved')
|
||||
|
||||
def test_itsa_book_year_suffix_uses_april_fiscal_start(self):
|
||||
'ITSA book year changes on April 1st'
|
||||
self.assertEqual(
|
||||
|
||||
@@ -15,6 +15,7 @@ depends:
|
||||
risk
|
||||
xml:
|
||||
global_reporting.xml
|
||||
coffee.xml
|
||||
purchase.xml
|
||||
contract_clause_reference.xml
|
||||
sale.xml
|
||||
@@ -26,8 +27,8 @@ xml:
|
||||
lc.xml
|
||||
optional.xml
|
||||
dashboard.xml
|
||||
fee.xml
|
||||
payment_term.xml
|
||||
fee.xml
|
||||
payment_term.xml
|
||||
purchase_prepayment.xml
|
||||
open_position.xml
|
||||
credit_risk.xml
|
||||
|
||||
101
modules/purchase_trade/view/coffee_sample_form.xml
Normal file
101
modules/purchase_trade/view/coffee_sample_form.xml
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0"?>
|
||||
<form col="4">
|
||||
<label name="reference"/>
|
||||
<field name="reference"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<newline/>
|
||||
<button name="receive" string="Receive"/>
|
||||
<button name="send_to_lab" string="Send to lab"/>
|
||||
<button name="review" string="Review"/>
|
||||
<button name="approve" string="Approve"/>
|
||||
<button name="reject" string="Reject"/>
|
||||
<button name="expire" string="Expire"/>
|
||||
<button name="archive" string="Archive"/>
|
||||
<button name="reset_to_requested" string="Reset"/>
|
||||
<newline/>
|
||||
<label name="direction"/>
|
||||
<field name="direction"/>
|
||||
<label name="sample_type"/>
|
||||
<field name="sample_type"/>
|
||||
<label name="purchase_line"/>
|
||||
<field name="purchase_line"/>
|
||||
<label name="sale_line"/>
|
||||
<field name="sale_line"/>
|
||||
<label name="lot"/>
|
||||
<field name="lot"/>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
<newline/>
|
||||
<separator string="Lifecycle" id="lifecycle" colspan="4"/>
|
||||
<label name="requested_date"/>
|
||||
<field name="requested_date"/>
|
||||
<label name="received_date"/>
|
||||
<field name="received_date"/>
|
||||
<label name="sent_date"/>
|
||||
<field name="sent_date"/>
|
||||
<label name="review_date"/>
|
||||
<field name="review_date"/>
|
||||
<label name="decision_date"/>
|
||||
<field name="decision_date"/>
|
||||
<label name="shelf_life_days"/>
|
||||
<field name="shelf_life_days"/>
|
||||
<label name="expiry_date"/>
|
||||
<field name="expiry_date"/>
|
||||
<label name="expires_soon"/>
|
||||
<field name="expires_soon"/>
|
||||
<newline/>
|
||||
<separator string="Sample" id="sample" colspan="4"/>
|
||||
<label name="quantity"/>
|
||||
<field name="quantity"/>
|
||||
<label name="unit"/>
|
||||
<field name="unit"/>
|
||||
<label name="storage_location"/>
|
||||
<field name="storage_location"/>
|
||||
<label name="delegated_to_lab"/>
|
||||
<field name="delegated_to_lab"/>
|
||||
<label name="lab"/>
|
||||
<field name="lab"/>
|
||||
<label name="lab_due_date"/>
|
||||
<field name="lab_due_date"/>
|
||||
<newline/>
|
||||
<separator string="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"/>
|
||||
<label name="screen_size"/>
|
||||
<field name="screen_size"/>
|
||||
<label name="defect_count"/>
|
||||
<field name="defect_count"/>
|
||||
<newline/>
|
||||
<separator string="Notes" id="notes" colspan="4"/>
|
||||
<field name="notes" colspan="4"/>
|
||||
<field name="decision_note" colspan="4"/>
|
||||
</form>
|
||||
13
modules/purchase_trade/view/coffee_sample_tree.xml
Normal file
13
modules/purchase_trade/view/coffee_sample_tree.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<tree>
|
||||
<field name="reference"/>
|
||||
<field name="direction"/>
|
||||
<field name="sample_type"/>
|
||||
<field name="state"/>
|
||||
<field name="party"/>
|
||||
<field name="product"/>
|
||||
<field name="received_date"/>
|
||||
<field name="expiry_date"/>
|
||||
<field name="total_score"/>
|
||||
<field name="lab"/>
|
||||
</tree>
|
||||
@@ -134,6 +134,28 @@ 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">
|
||||
<label name="coffee_quality_status"/>
|
||||
<field name="coffee_quality_status"/>
|
||||
<label name="coffee_origin"/>
|
||||
<field name="coffee_origin"/>
|
||||
<label name="coffee_process"/>
|
||||
<field name="coffee_process"/>
|
||||
<label name="coffee_variety"/>
|
||||
<field name="coffee_variety"/>
|
||||
<label name="coffee_crop_year"/>
|
||||
<field name="coffee_crop_year"/>
|
||||
<label name="coffee_screen_size"/>
|
||||
<field name="coffee_screen_size"/>
|
||||
<label name="coffee_moisture_max"/>
|
||||
<field name="coffee_moisture_max"/>
|
||||
<label name="coffee_defect_max"/>
|
||||
<field name="coffee_defect_max"/>
|
||||
<label name="coffee_cup_score_min"/>
|
||||
<field name="coffee_cup_score_min"/>
|
||||
<newline/>
|
||||
<field name="coffee_samples" colspan="4" mode="tree,form" view_ids="purchase_trade.coffee_sample_view_tree,purchase_trade.coffee_sample_view_form"/>
|
||||
</page>
|
||||
<page string="Premium composition" col="4" id="premium_composition">
|
||||
<field name="premium_decomposition" colspan="4" mode="tree,form" view_ids="purchase_trade.premium_composition_view_tree,purchase_trade.premium_composition_view_form"/>
|
||||
</page>
|
||||
|
||||
@@ -127,6 +127,28 @@ 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">
|
||||
<label name="coffee_quality_status"/>
|
||||
<field name="coffee_quality_status"/>
|
||||
<label name="coffee_origin"/>
|
||||
<field name="coffee_origin"/>
|
||||
<label name="coffee_process"/>
|
||||
<field name="coffee_process"/>
|
||||
<label name="coffee_variety"/>
|
||||
<field name="coffee_variety"/>
|
||||
<label name="coffee_crop_year"/>
|
||||
<field name="coffee_crop_year"/>
|
||||
<label name="coffee_screen_size"/>
|
||||
<field name="coffee_screen_size"/>
|
||||
<label name="coffee_moisture_max"/>
|
||||
<field name="coffee_moisture_max"/>
|
||||
<label name="coffee_defect_max"/>
|
||||
<field name="coffee_defect_max"/>
|
||||
<label name="coffee_cup_score_min"/>
|
||||
<field name="coffee_cup_score_min"/>
|
||||
<newline/>
|
||||
<field name="coffee_samples" colspan="4" mode="tree,form" view_ids="purchase_trade.coffee_sample_view_tree,purchase_trade.coffee_sample_view_form"/>
|
||||
</page>
|
||||
<page string="Fees" col="4" id="fees">
|
||||
<button name="apply_default_fees" icon="tryton-launch"/>
|
||||
<field name="fees" colspan="4" view_ids="purchase_trade.fee_view_tree_sequence"/>
|
||||
|
||||
Reference in New Issue
Block a user