diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 33ceecd..5fffb98 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -325,6 +325,8 @@ class Sale(metaclass=PoolMeta):
viewer = fields.Function(fields.Text(""),'get_viewer')
execution_summary_viewer = fields.Function(
fields.Text(""), 'get_execution_summary_viewer')
+ sale_summary_data = fields.Function(
+ fields.Text("Sale Summary"), 'get_sale_summary_data')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
@@ -1530,15 +1532,242 @@ class Sale(metaclass=PoolMeta):
except (TypeError, ValueError):
return 0.0
- def get_execution_summary_viewer(self, name=None):
+ @staticmethod
+ def _summary_decimal(value):
+ if value in (None, ''):
+ return Decimal('0')
+ try:
+ return Decimal(str(value))
+ except Exception:
+ return Decimal('0')
+
+ @classmethod
+ def _summary_percent(cls, part, total):
+ total = cls._summary_decimal(total)
+ if not total:
+ return 0.0
+ part = cls._summary_decimal(part)
+ percent = (part / total) * Decimal('100')
+ percent = max(Decimal('0'), min(Decimal('100'), percent))
+ return float(round(percent, 2))
+
+ @staticmethod
+ def _summary_number(value):
+ if value is None:
+ return None
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def _summary_link(model, records):
+ unique = []
+ seen = set()
+ for record in records:
+ record_id = getattr(record, 'id', None)
+ if not record_id:
+ continue
+ key = (model, record_id)
+ if key in seen:
+ continue
+ seen.add(key)
+ unique.append(record)
+ link = {
+ "model": model,
+ "count": len(unique),
+ }
+ if len(unique) == 1:
+ link["res_id"] = unique[0].id
+ return link
+
+ @staticmethod
+ def _summary_mixed_link(records, default_model=None):
+ unique = []
+ seen = set()
+ for model, record in records:
+ record_id = getattr(record, 'id', None)
+ if not model or not record_id:
+ continue
+ key = (model, record_id)
+ if key in seen:
+ continue
+ seen.add(key)
+ unique.append((model, record))
+ link = {"count": len(unique)}
+ if default_model:
+ link["model"] = default_model
+ if len(unique) == 1:
+ model, record = unique[0]
+ link.update({
+ "model": model,
+ "res_id": record.id,
+ })
+ elif unique:
+ link["model"] = unique[0][0]
+ return link
+
+ @staticmethod
+ def _summary_shipment_record(record):
+ if getattr(record, 'lot_shipment_out', None):
+ return ('stock.shipment.out', record.lot_shipment_out)
+ if getattr(record, 'lot_shipment_in', None):
+ return ('stock.shipment.in', record.lot_shipment_in)
+ if getattr(record, 'lot_shipment_internal', None):
+ return ('stock.shipment.internal', record.lot_shipment_internal)
+ return (None, None)
+
+ @classmethod
+ def _line_priced_percent(cls, line):
+ price_type = getattr(line, 'price_type', None)
+ if price_type in {'cash', 'priced', 'efp'}:
+ return 100.0
+ progress = getattr(line, 'progress', None)
+ if progress is None and hasattr(line, 'get_progress'):
+ progress = line.get_progress('progress')
+ return cls._summary_percent(progress or 0, 1)
+
+ @classmethod
+ def _line_hedged_percent(cls, line, quantity):
+ hedged_quantity = Decimal('0')
+ for derivative in (getattr(line, 'derivatives', None) or []):
+ der_quantity = getattr(derivative, 'quantity', None)
+ if der_quantity is None and hasattr(derivative, 'get_qt'):
+ der_quantity = derivative.get_qt('quantity')
+ hedged_quantity += abs(cls._summary_decimal(der_quantity))
+ return cls._summary_percent(hedged_quantity, quantity)
+
+ @classmethod
+ def _line_invoice_quantity_percent(cls, line, quantity, states=None):
+ invoiced_quantity = Decimal('0')
+ Uom = Pool().get('product.uom')
+ for invoice_line in (getattr(line, 'invoice_lines', None) or []):
+ invoice = getattr(invoice_line, 'invoice', None)
+ invoice_state = getattr(invoice, 'state', None)
+ if (not invoice or invoice_state == 'cancelled'
+ or (states and invoice_state not in states)
+ or getattr(invoice_line, 'type', 'line') != 'line'):
+ continue
+ invoice_quantity = cls._summary_decimal(
+ getattr(invoice_line, 'quantity', None))
+ invoice_unit = getattr(invoice_line, 'unit', None) or getattr(
+ line, 'unit', None)
+ line_unit = getattr(line, 'unit', None)
+ if invoice_unit and line_unit and invoice_unit != line_unit:
+ invoice_quantity = cls._summary_decimal(Uom.compute_qty(
+ invoice_unit, float(invoice_quantity), line_unit))
+ invoiced_quantity += abs(invoice_quantity)
+ return cls._summary_percent(invoiced_quantity, quantity)
+
+ @classmethod
+ def _line_period_name(cls, line):
+ period = getattr(line, 'del_period', None)
+ return getattr(period, 'rec_name', None) or getattr(period, 'name', '') or ''
+
+ @classmethod
+ def _line_amount_value(cls, line):
+ amount = getattr(line, 'amount', None)
+ if amount is None and hasattr(line, 'on_change_with_amount'):
+ amount = line.on_change_with_amount()
+ return cls._summary_decimal(amount)
+
+ @classmethod
+ def _line_pnl_values(cls, sale, line):
+ line_id = getattr(line, 'id', None)
+ pnl = Decimal('0')
+ mtm = Decimal('0')
+ previous_pnl = Decimal('0')
+ has_previous = False
+ records = (
+ getattr(sale, 'pnl', None)
+ if getattr(sale, 'group_pnl', False)
+ else getattr(sale, 'pnl_', None))
+ for record in (records or []):
+ record_line = getattr(record, 'r_sale_line', None) or getattr(
+ record, 'sale_line', None)
+ if line_id and getattr(record_line, 'id', None) != line_id:
+ continue
+ pnl += cls._summary_decimal(
+ getattr(record, 'r_pnl', None)
+ if hasattr(record, 'r_pnl') else getattr(record, 'pnl', None))
+ mtm += cls._summary_decimal(
+ getattr(record, 'r_mtm', None)
+ if hasattr(record, 'r_mtm') else getattr(record, 'mtm', None))
+ amount_prev = (
+ getattr(record, 'r_amount_prev', None)
+ if hasattr(record, 'r_amount_prev')
+ else getattr(record, 'amount_prev', None))
+ if amount_prev in (None, ''):
+ continue
+ base_amount = (
+ getattr(record, 'r_base_amount', None)
+ if hasattr(record, 'r_base_amount')
+ else getattr(record, 'base_amount', None))
+ previous_pnl += (
+ cls._summary_decimal(base_amount)
+ - cls._summary_decimal(amount_prev))
+ has_previous = True
+ return pnl, mtm, previous_pnl if has_previous else None
+
+ @classmethod
+ def _pnl_change_percent(cls, current, previous):
+ if previous in (None, ''):
+ return None
+ previous = cls._summary_decimal(previous)
+ if not previous:
+ return None
+ current = cls._summary_decimal(current)
+ return float(round(((current - previous) / abs(previous)) * 100, 2))
+
+ def _sale_pnl_change_percent(self):
+ changes = []
+ for line in (self.lines or []):
+ if getattr(line, 'type', 'line') != 'line':
+ continue
+ line_pnl, _line_mtm, previous_pnl = self._line_pnl_values(
+ self, line)
+ change = self._pnl_change_percent(line_pnl, previous_pnl)
+ if change is not None:
+ changes.append(Decimal(str(change)))
+ if not changes:
+ return None
+ return float(round(sum(changes) / Decimal(len(changes)), 2))
+
+ def _get_sale_map_summary_data(self):
+ dep_name = ''
+ arr_name = ''
+ departure = None
+ arrival = None
+ if self.from_location:
+ dep_name = self.from_location.name or self.from_location.rec_name
+ departure = {
+ "name": dep_name,
+ "lat": self._summary_number(self.from_location.lat),
+ "lon": self._summary_number(self.from_location.lon),
+ }
+ if self.to_location:
+ arr_name = self.to_location.name or self.to_location.rec_name
+ arrival = {
+ "name": arr_name,
+ "lat": self._summary_number(self.to_location.lat),
+ "lon": self._summary_number(self.to_location.lon),
+ }
+ return {
+ "from": dep_name,
+ "to": arr_name,
+ "route": '%s > %s' % (dep_name or '-', arr_name or '-'),
+ "departures": [departure] if departure else [],
+ "arrivals": [arrival] if arrival else [],
+ }
+
+ def _get_sale_execution_summary_data(self):
LotQt = Pool().get('lot.qt')
rows = []
totals = defaultdict(float)
- for line in (self.lines or []):
- if getattr(line, 'type', 'line') != 'line':
- continue
-
+ for index, line in enumerate((
+ l for l in (self.lines or [])
+ if getattr(l, 'type', 'line') == 'line'), 1):
quantity = self._execution_summary_quantity(
getattr(line, 'quantity_theorical', None)
or getattr(line, 'quantity', None))
@@ -1546,6 +1775,8 @@ class Sale(metaclass=PoolMeta):
shipped = 0.0
lots_count = 0
shipped_routes = []
+ purchases = []
+ shipments = []
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 []
@@ -1557,11 +1788,13 @@ class Sale(metaclass=PoolMeta):
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))
+ purchase_line = getattr(lqt.lot_p, 'line', None)
+ purchase = getattr(purchase_line, 'purchase', None)
+ if purchase:
+ purchases.append(purchase)
+ shipment_model, shipment = self._summary_shipment_record(lqt)
if shipment:
+ shipments.append((shipment_model, shipment))
shipped += lot_quantity
route_from = getattr(
getattr(shipment, 'from_location', None),
@@ -1584,11 +1817,15 @@ class Sale(metaclass=PoolMeta):
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))
+ purchase_line = getattr(lot, 'line', None)
+ if getattr(lot, 'lot_p', None):
+ purchase_line = getattr(lot.lot_p, 'line', None)
+ purchase = getattr(purchase_line, 'purchase', None)
+ if purchase:
+ purchases.append(purchase)
+ shipment_model, shipment = self._summary_shipment_record(lot)
if shipment:
+ shipments.append((shipment_model, shipment))
shipped += lot_quantity
route_from = getattr(
getattr(shipment, 'from_location', None),
@@ -1608,6 +1845,17 @@ class Sale(metaclass=PoolMeta):
unit = getattr(getattr(line, 'unit', None), 'rec_name', '') or ''
product = getattr(getattr(line, 'product', None), 'rec_name', '') or ''
+ invoices = [
+ invoice_line.invoice
+ for invoice_line in (getattr(line, 'invoice_lines', None) or [])
+ if (getattr(invoice_line, 'invoice', None)
+ and getattr(invoice_line.invoice, 'state', None) != 'cancelled')
+ ]
+ amount = self._line_amount_value(line)
+ line_pnl, line_mtm, previous_pnl = self._line_pnl_values(
+ self, line)
+ line_pnl_change = self._pnl_change_percent(
+ line_pnl, previous_pnl)
route = (shipped_routes[0] if shipped_routes else '%s > %s' % (
getattr(getattr(self, 'from_location', None), 'rec_name', None)
or '-',
@@ -1615,29 +1863,147 @@ class Sale(metaclass=PoolMeta):
or '-'))
rows.append({
+ 'index': index,
'product': product,
+ 'physical': shipped,
'quantity': quantity,
+ 'period': self._line_period_name(line),
'matched': matched,
'shipped': shipped,
+ 'priced': self._line_priced_percent(line),
+ 'hedged': self._line_hedged_percent(line, quantity),
+ 'invoiced': self._line_invoice_quantity_percent(line, quantity),
+ 'paid': self._line_invoice_quantity_percent(
+ line, quantity, states={'paid'}),
+ 'pnl': self._json_decimal(line_pnl),
+ 'pnl_change_d1': line_pnl_change,
+ 'amount': self._json_decimal(amount),
+ 'mtm': self._json_decimal(line_mtm),
'open': max(quantity - shipped, 0.0),
'unit': unit,
'lots': lots_count,
'route': route,
+ 'sales': self._summary_link('sale.sale', [self]),
+ 'purchases': self._summary_link('purchase.purchase', purchases),
+ 'shipments': self._summary_mixed_link(
+ shipments, default_model='stock.shipment.out'),
+ 'invoices': self._summary_link('account.invoice', invoices),
})
totals['quantity'] += quantity
totals['matched'] += matched
totals['shipped'] += shipped
totals['open'] += max(quantity - shipped, 0.0)
totals['lines'] += 1
+ totals['amount'] += float(amount)
+ totals['pnl'] += float(line_pnl)
+ totals['mtm'] += float(line_mtm)
- data = {
- 'type': 'execution-summary',
+ return {
'unit': rows[0]['unit'] if rows else '',
'totals': dict(totals),
- 'rows': rows[:4],
+ 'rows': rows[:3],
}
+
+ @staticmethod
+ def _json_decimal(value):
+ if value in (None, ''):
+ return None
+ return float(value)
+
+ def _get_sale_pnl_summary_data(self):
+ currency = getattr(getattr(self, 'currency', None), 'code', None) or ''
+ pnl_value = Decimal('0')
+ mtm_value = Decimal('0')
+ records = (
+ getattr(self, 'pnl', None)
+ if getattr(self, 'group_pnl', False)
+ else getattr(self, 'pnl_', None))
+ for line in (records or []):
+ pnl_value += self._summary_decimal(
+ getattr(line, 'r_pnl', None)
+ if hasattr(line, 'r_pnl') else getattr(line, 'pnl', None))
+ mtm_value += self._summary_decimal(
+ getattr(line, 'r_mtm', None)
+ if hasattr(line, 'r_mtm') else getattr(line, 'mtm', None))
+ data = {
+ "pnl": {
+ "value": self._json_decimal(pnl_value),
+ "currency": currency,
+ "change_d1": self._sale_pnl_change_percent(),
+ },
+ "mtm": {
+ "value": self._json_decimal(mtm_value),
+ "currency": currency,
+ "change": None,
+ },
+ }
+ return data
+
+ def _get_sale_activity_summary_data(self):
+ if not getattr(self, 'id', None):
+ return []
+ Log = Pool().get('ir.model.log')
+ logs = Log.search([
+ ('resource', '=', 'sale.sale,%s' % self.id),
+ ('event', '!=', 'create'),
+ ], limit=8)
+ event_types = {
+ 'write': 'info',
+ 'transition': 'approved',
+ 'button': 'success',
+ 'wizard': 'info',
+ 'delete': 'warning',
+ }
+ activity = []
+ for log in logs:
+ if len(activity) >= 4:
+ break
+ date = getattr(log, 'create_date', None)
+ user = getattr(getattr(log, 'user', None), 'rec_name', None) or ''
+ text = getattr(log, 'change_summary', None)
+ text = text or getattr(log, 'action', None) or getattr(
+ log, 'target', None)
+ if not text:
+ text = getattr(log, 'event_string', None) or log.event
+ if ' create' in text or 'create,' in text or text.startswith('Create'):
+ continue
+ activity.append({
+ "type": event_types.get(log.event, 'info'),
+ "text": text,
+ "date": date.strftime('%d/%m/%Y') if date else '',
+ "user": user,
+ })
+ return activity
+
+ def get_execution_summary_viewer(self, name=None):
+ data = self._get_sale_execution_summary_data()
+ data = dict(data, type='execution-summary')
return "d3:" + json.dumps(data)
+ def get_sale_summary_data(self, name=None):
+ execution = self._get_sale_execution_summary_data()
+ data = {
+ "currency": getattr(getattr(self, 'currency', None), 'code', None)
+ or '',
+ "map": self._get_sale_map_summary_data(),
+ "execution": execution,
+ "amount": {
+ "value": execution.get('totals', {}).get('amount', 0),
+ },
+ "activity": self._get_sale_activity_summary_data(),
+ "approval": {
+ "status": "need_approval",
+ },
+ }
+ data.update(self._get_sale_pnl_summary_data())
+ return json.dumps(data)
+
+ @fields.depends(
+ 'currency', 'from_location', 'to_location', 'lines', 'pnl', 'pnl_',
+ 'group_pnl')
+ def on_change_with_sale_summary_data(self, name=None):
+ return self.get_sale_summary_data()
+
def getLots(self):
if self.lines:
if self.lines.lots:
diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml
index 802c0cf..79ea80b 100755
--- a/modules/purchase_trade/view/purchase_form.xml
+++ b/modules/purchase_trade/view/purchase_form.xml
@@ -103,26 +103,6 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/modules/purchase_trade/view/sale_form.xml b/modules/purchase_trade/view/sale_form.xml
index 5b63e5c..b014fb0 100755
--- a/modules/purchase_trade/view/sale_form.xml
+++ b/modules/purchase_trade/view/sale_form.xml
@@ -3,10 +3,21 @@
this repository contains the full copyright notices and license terms. -->
+
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -153,24 +146,6 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-