from decimal import Decimal, ROUND_HALF_UP from trytond.pool import Pool, PoolMeta from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words from trytond.exceptions import UserError from trytond.modules.account_invoice.invoice import ( InvoiceReport as BaseInvoiceReport) from trytond.modules.sale.sale import SaleReport as BaseSaleReport from trytond.modules.purchase.purchase import ( PurchaseReport as BasePurchaseReport) class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' @staticmethod def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False, strip_trailing_zeros=True): value = Decimal(str(value or 0)).quantize(Decimal(digits)) text = format(value, 'f') if strip_trailing_zeros: text = text.rstrip('0').rstrip('.') if keep_trailing_decimal and '.' not in text: text += '.0' return text or '0' def _get_report_invoice_line(self): for line in self.lines or []: if getattr(line, 'type', None) == 'line': return line return self.lines[0] if self.lines else None def _get_report_invoice_lines(self): lines = [ line for line in (self.lines or []) if getattr(line, 'type', None) == 'line' ] return lines or list(self.lines or []) @staticmethod def _clean_report_description(value): text = (value or '').strip() normalized = text.replace(' ', '').upper() if normalized == 'PROFORMA': return '' return text.upper() if text else '' def _get_report_purchase(self): purchases = list(self.purchases or []) return purchases[0] if purchases else None def _get_report_sale(self): # Bridge invoice templates to the originating sale so FODT files can # reuse stable sale.report_* properties instead of complex expressions. sales = list(self.sales or []) return sales[0] if sales else None def _get_report_trade(self): return self._get_report_sale() or self._get_report_purchase() def _get_report_purchase_line(self): purchase = self._get_report_purchase() if purchase and purchase.lines: return purchase.lines[0] def _get_report_sale_line(self): sale = self._get_report_sale() if sale and sale.lines: return sale.lines[0] def _get_report_trade_line(self): return self._get_report_sale_line() or self._get_report_purchase_line() def _get_report_lot(self): line = self._get_report_trade_line() if line and line.lots: for lot in line.lots: if lot.lot_type == 'physic': return lot return line.lots[0] def _get_report_invoice_lots(self): invoice_lines = self._get_report_invoice_lines() if not invoice_lines: return [] def _same_invoice_line(left, right): if not left or not right: return False left_id = getattr(left, 'id', None) right_id = getattr(right, 'id', None) if left_id is not None and right_id is not None: return left_id == right_id return left is right trade = self._get_report_trade() trade_lines = getattr(trade, 'lines', []) if trade else [] lots = [] for line in trade_lines or []: for lot in getattr(line, 'lots', []) or []: if getattr(lot, 'lot_type', None) != 'physic': continue refs = [ getattr(lot, 'sale_invoice_line', None), getattr(lot, 'sale_invoice_line_prov', None), getattr(lot, 'invoice_line', None), getattr(lot, 'invoice_line_prov', None), ] if any( _same_invoice_line(ref, invoice_line) for ref in refs for invoice_line in invoice_lines): lots.append(lot) return lots @staticmethod def _format_report_package_label(unit): label = ( getattr(unit, 'symbol', None) or getattr(unit, 'rec_name', None) or getattr(unit, 'name', None) or 'BALE' ) label = label.upper() if not label.endswith('S'): label += 'S' return label def _get_report_freight_fee(self): pool = Pool() Fee = pool.get('fee.fee') shipment = self._get_report_shipment() if not shipment: return None fees = Fee.search([ ('shipment_in', '=', shipment.id), ('product.name', '=', 'Maritime freight'), ], limit=1) return fees[0] if fees else None def _get_report_shipment(self): lot = self._get_report_lot() if not lot: return None return ( getattr(lot, 'lot_shipment_in', None) or getattr(lot, 'lot_shipment_out', None) or getattr(lot, 'lot_shipment_internal', None) ) @property def report_address(self): trade = self._get_report_trade() if trade and trade.report_address: return trade.report_address if self.invoice_address and self.invoice_address.full_address: return self.invoice_address.full_address return '' @property def report_contract_number(self): trade = self._get_report_trade() if trade and trade.full_number: return trade.full_number return self.origins or '' @property def report_shipment(self): trade = self._get_report_trade() if trade and trade.report_shipment: return trade.report_shipment return self.description or '' @property def report_trader_initial(self): trade = self._get_report_trade() if trade and getattr(trade, 'trader', None): return trade.trader.initial or '' return '' @property def report_origin(self): trade = self._get_report_trade() if trade and getattr(trade, 'product_origin', None): return trade.product_origin or '' return '' @property def report_operator_initial(self): trade = self._get_report_trade() if trade and getattr(trade, 'operator', None): return trade.operator.initial or '' return '' @property def report_product_description(self): line = self._get_report_trade_line() if line and line.product: return line.product.description or '' return '' @property def report_description_upper(self): if self.lines: return self._clean_report_description(self.lines[0].description) return '' @property def report_crop_name(self): trade = self._get_report_trade() if trade and getattr(trade, 'crop', None): return trade.crop.name or '' return '' @property def report_attributes_name(self): line = self._get_report_trade_line() if line: return getattr(line, 'attributes_name', '') or '' return '' @property def report_price(self): trade = self._get_report_trade() if trade and trade.report_price: return trade.report_price return '' @property def report_quantity_lines(self): details = [] for line in self._get_report_invoice_lines(): quantity = getattr(line, 'report_net', '') if quantity == '': quantity = getattr(line, 'quantity', '') if quantity == '': continue quantity_text = self._format_report_number( quantity, keep_trailing_decimal=True) unit = getattr(line, 'unit', None) unit_name = unit.rec_name.upper() if unit and unit.rec_name else '' lbs = getattr(line, 'report_lbs', '') parts = [quantity_text, unit_name] if lbs != '': parts.append( f"({self._format_report_number(lbs, digits='0.01')} LBS)") detail = ' '.join(part for part in parts if part) if detail: details.append(detail) return '\n'.join(details) @property def report_trade_blocks(self): blocks = [] quantity_lines = self.report_quantity_lines.splitlines() rate_lines = self.report_rate_lines.splitlines() for index, quantity_line in enumerate(quantity_lines): price_line = rate_lines[index] if index < len(rate_lines) else '' blocks.append((quantity_line, price_line)) return blocks @property def report_rate_currency_upper(self): line = self._get_report_invoice_line() if line: return line.report_rate_currency_upper return '' @property def report_rate_value(self): line = self._get_report_invoice_line() if line: return line.report_rate_value return '' @property def report_rate_unit_upper(self): line = self._get_report_invoice_line() if line: return line.report_rate_unit_upper return '' @property def report_rate_price_words(self): line = self._get_report_invoice_line() if line: return line.report_rate_price_words return self.report_price or '' @property def report_rate_pricing_text(self): line = self._get_report_invoice_line() if line: return line.report_rate_pricing_text return '' @property def report_rate_lines(self): details = [] for line in self._get_report_invoice_lines(): currency = getattr(line, 'report_rate_currency_upper', '') or '' value = getattr(line, 'report_rate_value', '') value_text = '' if value != '': value_text = self._format_report_number( value, strip_trailing_zeros=False) unit = getattr(line, 'report_rate_unit_upper', '') or '' words = getattr(line, 'report_rate_price_words', '') or '' pricing_text = getattr(line, 'report_rate_pricing_text', '') or '' detail = ' '.join( part for part in [ currency, value_text, 'PER' if unit else '', unit, f"({words})" if words else '', pricing_text, ] if part) if detail: details.append(detail) return '\n'.join(details) @property def report_positive_rate_lines(self): sale = self._get_report_sale() if sale and getattr(sale, 'report_price_lines', None): return sale.report_price_lines details = [] for line in self._get_report_invoice_lines(): quantity = getattr(line, 'report_net', '') if quantity == '': quantity = getattr(line, 'quantity', '') if Decimal(str(quantity or 0)) <= 0: continue currency = getattr(line, 'report_rate_currency_upper', '') or '' value = getattr(line, 'report_rate_value', '') value_text = '' if value != '': value_text = self._format_report_number( value, strip_trailing_zeros=False) unit = getattr(line, 'report_rate_unit_upper', '') or '' words = getattr(line, 'report_rate_price_words', '') or '' pricing_text = getattr(line, 'report_rate_pricing_text', '') or '' detail = ' '.join( part for part in [ currency, value_text, 'PER' if unit else '', unit, f"({words})" if words else '', pricing_text, ] if part) if detail: details.append(detail) return '\n'.join(details) @property def report_payment_date(self): trade = self._get_report_trade() if trade and trade.report_payment_date: return trade.report_payment_date return '' @property def report_delivery_period_description(self): trade = self._get_report_trade() if trade and getattr(trade, 'report_delivery_period_description', None): return trade.report_delivery_period_description line = self._get_report_trade_line() if line and getattr(line, 'del_period', None): return line.del_period.description or '' return '' @property def report_payment_description(self): trade = self._get_report_trade() if trade and trade.payment_term: return trade.payment_term.description or '' if self.payment_term: return self.payment_term.description or '' return '' @property def report_nb_bale(self): total_packages = Decimal(0) package_unit = None has_invoice_line_packages = False for line in self._get_report_invoice_lines(): lot = getattr(line, 'lot', None) if not lot or getattr(lot, 'lot_qt', None) in (None, ''): continue has_invoice_line_packages = True if not package_unit and getattr(lot, 'lot_unit', None): package_unit = lot.lot_unit sign = Decimal(1) if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0: sign = Decimal(-1) total_packages += ( Decimal(str(lot.lot_qt or 0)).quantize( Decimal('1'), rounding=ROUND_HALF_UP) * sign) if has_invoice_line_packages: label = self._format_report_package_label(package_unit) return f"NB {label}: {int(total_packages)}" lots = self._get_report_invoice_lots() if lots: total_packages = Decimal(0) package_unit = None for lot in lots: if getattr(lot, 'lot_qt', None): total_packages += Decimal(str(lot.lot_qt or 0)) if not package_unit and getattr(lot, 'lot_unit', None): package_unit = lot.lot_unit package_qty = total_packages.quantize( Decimal('1'), rounding=ROUND_HALF_UP) label = self._format_report_package_label(package_unit) return f"NB {label}: {int(package_qty)}" sale = self._get_report_sale() if sale and sale.report_nb_bale: return sale.report_nb_bale line = self._get_report_trade_line() if line and line.lots: nb_bale = sum( lot.lot_qt for lot in line.lots if lot.lot_type == 'physic' ) return 'NB BALES: ' + str(int(nb_bale)) return '' @property def report_gross(self): if self.lines: return sum( Decimal(str(getattr(line, 'quantity', 0) or 0)) for line in self._get_report_invoice_lines()) line = self._get_report_trade_line() if line and line.lots: return sum( lot.get_current_gross_quantity() for lot in line.lots if lot.lot_type == 'physic' ) return '' @property def report_net(self): if self.lines: return sum( Decimal(str(getattr(line, 'quantity', 0) or 0)) for line in self._get_report_invoice_lines()) line = self._get_report_trade_line() if line and line.lots: return sum( lot.get_current_quantity() for lot in line.lots if lot.lot_type == 'physic' ) if self.lines: return self.lines[0].quantity return '' @property def report_lbs(self): net = self.report_net if net == '': return '' return round(Decimal(net) * Decimal('2204.62'),2) @property def report_weight_unit_upper(self): line = self._get_report_trade_line() or self._get_report_invoice_line() unit = getattr(line, 'unit', None) if line else None if unit and unit.rec_name: return unit.rec_name.upper() return 'KGS' @property def report_note_title(self): total = Decimal(str(self.total_amount or 0)) if total < 0: return 'Debit Note' return 'Credit Note' @property def report_bl_date(self): shipment = self._get_report_shipment() if shipment: return shipment.bl_date @property def report_bl_nb(self): shipment = self._get_report_shipment() if shipment: return shipment.bl_number @property def report_vessel(self): shipment = self._get_report_shipment() if shipment and shipment.vessel: return shipment.vessel.vessel_name @property def report_loading_port(self): shipment = self._get_report_shipment() if shipment and shipment.from_location: return shipment.from_location.rec_name return '' @property def report_discharge_port(self): shipment = self._get_report_shipment() if shipment and shipment.to_location: return shipment.to_location.rec_name return '' @property def report_incoterm(self): trade = self._get_report_trade() if not trade: return '' incoterm = trade.incoterm.code if getattr(trade, 'incoterm', None) else '' location = ( trade.incoterm_location.party_name if getattr(trade, 'incoterm_location', None) else '' ) if incoterm and location: return f"{incoterm} {location}" return incoterm or location @property def report_proforma_invoice_number(self): lot = self._get_report_lot() if lot: line = ( getattr(lot, 'sale_invoice_line_prov', None) or getattr(lot, 'invoice_line_prov', None) ) if line and line.invoice: return line.invoice.number or '' return '' @property def report_proforma_invoice_date(self): lot = self._get_report_lot() if lot: line = ( getattr(lot, 'sale_invoice_line_prov', None) or getattr(lot, 'invoice_line_prov', None) ) if line and line.invoice: return line.invoice.invoice_date @property def report_controller_name(self): shipment = self._get_report_shipment() if shipment and shipment.controller: return shipment.controller.rec_name return '' @property def report_si_number(self): shipment = self._get_report_shipment() if shipment: return shipment.number or '' return '' @property def report_freight_amount(self): fee = self._get_report_freight_fee() if fee: return fee.get_amount() return '' @property def report_freight_currency_symbol(self): fee = self._get_report_freight_fee() if fee and fee.currency: return fee.currency.symbol or '' if self.currency: return self.currency.symbol or '' return 'USD' class InvoiceLine(metaclass=PoolMeta): __name__ = 'account.invoice.line' def _get_report_trade(self): origin = getattr(self, 'origin', None) if not origin: return None return getattr(origin, 'sale', None) or getattr(origin, 'purchase', None) def _get_report_trade_line(self): return getattr(self, 'origin', None) @property def report_product_description(self): if self.product: return self.product.description or '' origin = getattr(self, 'origin', None) if origin and getattr(origin, 'product', None): return origin.product.description or '' return '' @property def report_description_upper(self): return Invoice._clean_report_description(self.description) @property def report_rate_currency_upper(self): origin = self._get_report_trade_line() currency = getattr(origin, 'linked_currency', None) or self.currency if currency and currency.rec_name: return currency.rec_name.upper() return '' @property def report_rate_value(self): origin = self._get_report_trade_line() if origin and getattr(origin, 'price_type', None) == 'basis': if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None): return Decimal(str(origin.premium or 0)) return Decimal(str(origin._get_premium_price() or 0)) return self.unit_price if self.unit_price is not None else '' @property def report_rate_unit_upper(self): origin = self._get_report_trade_line() unit = getattr(origin, 'linked_unit', None) or self.unit if unit and unit.rec_name: return unit.rec_name.upper() return '' @property def report_rate_price_words(self): origin = self._get_report_trade_line() if origin and getattr(origin, 'price_type', None) == 'basis': value = self.report_rate_value if self.report_rate_currency_upper == 'USC': return amount_to_currency_words(value, 'USC', 'USC') return amount_to_currency_words(value) trade = self._get_report_trade() if trade and getattr(trade, 'report_price', None): return trade.report_price return '' @property def report_rate_pricing_text(self): origin = self._get_report_trade_line() return getattr(origin, 'get_pricing_text', '') or '' @property def report_crop_name(self): trade = self._get_report_trade() if trade and getattr(trade, 'crop', None): return trade.crop.name or '' return '' @property def report_attributes_name(self): origin = getattr(self, 'origin', None) if origin: return getattr(origin, 'attributes_name', '') or '' return '' @property def report_net(self): if self.type == 'line': return self.quantity return '' @property def report_lbs(self): net = self.report_net if net == '': return '' return round(Decimal(net) * Decimal('2204.62'),2) class ReportTemplateMixin: @classmethod def _get_purchase_trade_configuration(cls): Configuration = Pool().get('purchase_trade.configuration') configurations = Configuration.search([], limit=1) return configurations[0] if configurations else None @classmethod def _get_action_name(cls, action): if isinstance(action, dict): return action.get('name') or '' return getattr(action, 'name', '') or '' @classmethod def _get_action_report_path(cls, action): if isinstance(action, dict): return action.get('report') or '' return getattr(action, 'report', '') or '' @classmethod def _resolve_template_path(cls, action, field_name, default_prefix): config = cls._get_purchase_trade_configuration() template = getattr(config, field_name, '') if config else '' template = (template or '').strip() if not template: raise UserError('No template found') if '/' not in template: return f'{default_prefix}/{template}' return template @classmethod def _get_resolved_action(cls, action): report_path = cls._resolve_configured_report_path(action) if isinstance(action, dict): resolved = dict(action) resolved['report'] = report_path return resolved setattr(action, 'report', report_path) return action @classmethod def _execute(cls, records, header, data, action): resolved_action = cls._get_resolved_action(action) return super()._execute(records, header, data, resolved_action) class InvoiceReport(ReportTemplateMixin, BaseInvoiceReport): __name__ = 'account.invoice' @classmethod def _resolve_configured_report_path(cls, action): report_path = cls._get_action_report_path(action) or '' action_name = cls._get_action_name(action) if (report_path.endswith('/prepayment.fodt') or action_name == 'Prepayment'): field_name = 'invoice_prepayment_report_template' elif (report_path.endswith('/invoice_ict_final.fodt') or action_name == 'CN/DN'): field_name = 'invoice_cndn_report_template' else: field_name = 'invoice_report_template' return cls._resolve_template_path(action, field_name, 'account_invoice') class SaleReport(ReportTemplateMixin, BaseSaleReport): __name__ = 'sale.sale' @classmethod def _resolve_configured_report_path(cls, action): report_path = cls._get_action_report_path(action) action_name = cls._get_action_name(action) if report_path.endswith('/bill.fodt') or action_name == 'Bill': field_name = 'sale_bill_report_template' elif report_path.endswith('/sale_final.fodt') or action_name == 'Sale (final)': field_name = 'sale_final_report_template' else: field_name = 'sale_report_template' return cls._resolve_template_path(action, field_name, 'sale') class PurchaseReport(ReportTemplateMixin, BasePurchaseReport): __name__ = 'purchase.purchase' @classmethod def _resolve_configured_report_path(cls, action): return cls._resolve_template_path( action, 'purchase_report_template', 'purchase')