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. --> + + + + + @@ -18,100 +29,82 @@ this repository contains the full copyright notices and license terms. -->