Sale design

This commit is contained in:
2026-06-08 21:44:08 +02:00
parent 5838d848c2
commit 3a6f426033
2 changed files with 225 additions and 51 deletions

View File

@@ -15,6 +15,7 @@ from sql.functions import CurrentTimestamp, DateTrunc
import datetime
import logging
import json
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
@@ -279,6 +280,8 @@ class Sale(metaclass=PoolMeta):
association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),})
crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
viewer = fields.Function(fields.Text(""),'get_viewer')
execution_summary_viewer = fields.Function(
fields.Text(""), 'get_execution_summary_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
@@ -1186,6 +1189,122 @@ class Sale(metaclass=PoolMeta):
def on_change_with_viewer(self):
return self.get_viewer()
def _execution_summary_quantity(self, value):
if value is None:
return 0.0
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def get_execution_summary_viewer(self, name=None):
LotQt = Pool().get('lot.qt')
rows = []
totals = defaultdict(float)
for line in (self.lines or []):
if getattr(line, 'type', 'line') != 'line':
continue
quantity = self._execution_summary_quantity(
getattr(line, 'quantity_theorical', None)
or getattr(line, 'quantity', None))
matched = 0.0
shipped = 0.0
lots_count = 0
shipped_routes = []
line_lots = list(getattr(line, 'lots', None) or [])
lot_ids = [lot.id for lot in line_lots if getattr(lot, 'id', None)]
lot_qts = LotQt.search([('lot_s', 'in', lot_ids)]) if lot_ids else []
for lqt in lot_qts:
lots_count += 1
lot_quantity = self._execution_summary_quantity(
getattr(lqt, 'lot_quantity', None)
or getattr(lqt, 'lot_qt', None))
if getattr(lqt, 'lot_p', None):
matched += lot_quantity
shipment = (
getattr(lqt, 'lot_shipment_in', None)
or getattr(lqt, 'lot_shipment_internal', None)
or getattr(lqt, 'lot_shipment_out', None))
if shipment:
shipped += lot_quantity
route_from = getattr(
getattr(shipment, 'from_location', None),
'rec_name', None)
route_to = getattr(
getattr(shipment, 'to_location', None),
'rec_name', None)
route_from = route_from or getattr(
getattr(self, 'from_location', None), 'rec_name', None)
route_to = route_to or getattr(
getattr(self, 'to_location', None), 'rec_name', None)
if route_from or route_to:
shipped_routes.append(
'%s > %s' % (route_from or '-', route_to or '-'))
if not lot_qts:
for lot in line_lots:
lots_count += 1
lot_quantity = self._execution_summary_quantity(
getattr(lot, 'lot_qt', None))
if getattr(lot, 'lot_p', None) or getattr(lot, 'line', None):
matched += lot_quantity
shipment = (
getattr(lot, 'lot_shipment_in', None)
or getattr(lot, 'lot_shipment_internal', None)
or getattr(lot, 'lot_shipment_out', None))
if shipment:
shipped += lot_quantity
route_from = getattr(
getattr(shipment, 'from_location', None),
'rec_name', None)
route_to = getattr(
getattr(shipment, 'to_location', None),
'rec_name', None)
route_from = route_from or getattr(
getattr(self, 'from_location', None),
'rec_name', None)
route_to = route_to or getattr(
getattr(self, 'to_location', None),
'rec_name', None)
if route_from or route_to:
shipped_routes.append(
'%s > %s' % (route_from or '-', route_to or '-'))
unit = getattr(getattr(line, 'unit', None), 'rec_name', '') or ''
product = getattr(getattr(line, 'product', None), 'rec_name', '') or ''
route = (shipped_routes[0] if shipped_routes else '%s > %s' % (
getattr(getattr(self, 'from_location', None), 'rec_name', None)
or '-',
getattr(getattr(self, 'to_location', None), 'rec_name', None)
or '-'))
rows.append({
'product': product,
'quantity': quantity,
'matched': matched,
'shipped': shipped,
'open': max(quantity - shipped, 0.0),
'unit': unit,
'lots': lots_count,
'route': route,
})
totals['quantity'] += quantity
totals['matched'] += matched
totals['shipped'] += shipped
totals['open'] += max(quantity - shipped, 0.0)
totals['lines'] += 1
data = {
'type': 'execution-summary',
'unit': rows[0]['unit'] if rows else '',
'totals': dict(totals),
'rows': rows[:4],
}
return "d3:" + json.dumps(data)
def getLots(self):
if self.lines:
if self.lines.lots:

View File

@@ -2,57 +2,112 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/group/field[@name='currency']" position="after">
<newline/>
<label name="lc_date"/>
<field name="lc_date"/>
<label name="wb"/>
<field name="wb"/>
<newline/>
<label name="certif"/>
<field name="certif"/>
<newline/>
<label name="agent"/>
<field name="agent"/>
<label name="operator"/>
<field name="operator"/>
<newline/>
<label name="trader"/>
<field name="trader"/>
<label name="our_reference"/>
<field name="our_reference"/>
<newline/>
<label name="association"/>
<field name="association"/>
<label name="crop"/>
<field name="crop"/>
<newline/>
<label name="tol_min"/>
<field name="tol_min"/>
<label name="tol_max"/>
<field name="tol_max"/>
<newline/>
<label name="tolerance_used"/>
<field name="tolerance_used" widget="tolerance_gauge"
min_field="tolerance_min" max_field="tolerance_max"
xexpand="1" xfill="1"/>
<label name="tolerance_option"/>
<field name="tolerance_option"/>
<field name="tolerance_min" invisible="1"/>
<field name="tolerance_max" invisible="1"/>
<newline/>
<label name="from_location"/>
<field name="from_location"/>
<label name="to_location"/>
<field name="to_location"/>
<newline/>
<label name="incoterm"/>
<field name="incoterm"/>
<label name="incoterm_location"/>
<field name="incoterm_location"/>
<newline/>
<label name="product_origin"/>
<field name="product_origin"/>
<xpath expr="/form/group[@id='hd'][2]" position="replace"/>
<xpath expr="/form/group[@id='hd'][1]" position="replace">
<group id="sale_header_summary" colspan="6" col="10"
panel="summary" xalign="0" yalign="0"
col_widths="min-content,2fr,min-content,130px,min-content,1.2fr,min-content,130px,min-content,1fr">
<label name="party"/>
<field name="party"/>
<label name="number"/>
<field name="number"/>
<label name="reference"/>
<field name="reference"/>
<label name="sale_date"/>
<field name="sale_date"/>
<label name="currency"/>
<field name="currency"/>
</group>
<group id="sale_header_left" colspan="4" col="1"
xalign="0" yalign="0" col_widths="1fr">
<group id="sale_commercial_terms" string="Commercial Terms"
colspan="1" col="1" panel="card" icon="tryton-public"
xalign="0" yalign="0"
col_widths="1fr">
<group id="sale_commercial_terms_top"
colspan="1" col="6" xalign="0" yalign="0"
col_widths="min-content,1fr,min-content,1fr,min-content,130px">
<label name="payment_term"/>
<field name="payment_term"/>
<label name="wb"/>
<field name="wb"/>
<label name="lc_date"/>
<field name="lc_date"/>
</group>
<group id="sale_commercial_terms_incoterm"
colspan="1" col="4" xalign="0" yalign="0"
col_widths="min-content,1fr,min-content,1fr">
<label name="incoterm"/>
<field name="incoterm"/>
<label name="incoterm_location"/>
<field name="incoterm_location"/>
</group>
<group id="sale_commercial_terms_origin"
colspan="1" col="4" xalign="0" yalign="0"
col_widths="min-content,1fr,min-content,1fr">
<label name="product_origin"/>
<field name="product_origin"/>
<label name="tolerance_option"/>
<field name="tolerance_option"/>
</group>
<field name="certif" invisible="1"/>
</group>
<group id="sale_execution" string="People &amp; Execution"
colspan="1" col="4" panel="card" icon="tryton-link"
xalign="0" yalign="0"
col_widths="min-content,1fr,min-content,1fr">
<label name="broker"/>
<field name="broker"/>
<label name="operator"/>
<field name="operator"/>
<label name="trader"/>
<field name="trader"/>
<label name="our_reference"/>
<field name="our_reference"/>
<label name="from_location"/>
<field name="from_location"/>
<label name="to_location"/>
<field name="to_location"/>
<label name="association"/>
<field name="association"/>
<label name="crop"/>
<field name="crop"/>
</group>
<group id="sale_tolerance" string="Tolerance"
colspan="1" col="6" panel="card" icon="tryton-switch"
xalign="0" yalign="0"
col_widths="min-content,1fr,min-content,1fr,min-content,200px">
<label name="tol_min"/>
<field name="tol_min"/>
<label name="tol_max"/>
<field name="tol_max"/>
<label name="tolerance_used"/>
<field name="tolerance_used" widget="tolerance_gauge"
min_field="tolerance_min" max_field="tolerance_max"
width="190" xexpand="0" xfill="0"/>
</group>
<field name="tolerance_min" invisible="1"/>
<field name="tolerance_max" invisible="1"/>
</group>
<group id="sale_header_right" colspan="2" col="1"
xalign="0" yalign="0" col_widths="1fr">
<group id="sale_map" string="Map" colspan="1" col="1"
panel="card" icon="tryton-public" xalign="0" yalign="0"
col_widths="1fr">
<field name="viewer" widget="html_viewer" height="220"/>
</group>
<group id="sale_recap" string="Execution summary"
colspan="1" col="1" panel="sidebar" icon="tryton-note"
xalign="0" yalign="0"
col_widths="1fr">
<field name="execution_summary_viewer" widget="html_viewer"
height="142"/>
</group>
</group>
</xpath>
<xpath expr="/form/notebook/page[@id='sale']" position="after">
<page string="Contract Clauses" col="4" id="contract_clauses">