738 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			738 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# This file is part of facho.  The COPYRIGHT file at the top level of
 | 
						|
# this repository contains the full copyright notices and license terms.
 | 
						|
 | 
						|
import hashlib
 | 
						|
from functools import reduce
 | 
						|
import copy
 | 
						|
import dataclasses
 | 
						|
from dataclasses import dataclass
 | 
						|
from datetime import datetime, date
 | 
						|
from collections import defaultdict
 | 
						|
import decimal
 | 
						|
from decimal import Decimal
 | 
						|
import typing
 | 
						|
 | 
						|
from ..data.dian import codelist
 | 
						|
 | 
						|
DECIMAL_PRECISION = 6
 | 
						|
 | 
						|
class AmountCurrencyError(TypeError):
 | 
						|
    pass
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Currency:
 | 
						|
    code: str
 | 
						|
 | 
						|
    def __eq__(self, other):
 | 
						|
        return self.code == other.code
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.code
 | 
						|
 | 
						|
class Collection:
 | 
						|
 | 
						|
    def __init__(self, array):
 | 
						|
        self.array = array
 | 
						|
 | 
						|
    def filter(self, filterer):
 | 
						|
        new_array = filter(filterer, self.array)
 | 
						|
        return self.__class__(new_array)
 | 
						|
 | 
						|
    def map(self, mapper):
 | 
						|
        new_array = map(mapper, self.array)
 | 
						|
        return self.__class__(new_array)
 | 
						|
 | 
						|
    def sum(self):
 | 
						|
        return sum(self.array)
 | 
						|
 | 
						|
class AmountCollection(Collection):
 | 
						|
 | 
						|
    def sum(self):
 | 
						|
        total = Amount(0)
 | 
						|
        for v in self.array:
 | 
						|
            total += v
 | 
						|
        return total
 | 
						|
 | 
						|
class Amount:
 | 
						|
    def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')):
 | 
						|
 | 
						|
        #DIAN 1.7.-2020: 1.2.3.1
 | 
						|
        if isinstance(amount, Amount):
 | 
						|
            if amount < Amount(0.0):
 | 
						|
                raise ValueError('amount must be positive >= 0')
 | 
						|
 | 
						|
            self.amount = amount.amount
 | 
						|
            self.currency = amount.currency
 | 
						|
        else:
 | 
						|
            if float(amount) < 0:
 | 
						|
                raise ValueError('amount must be positive >= 0')
 | 
						|
 | 
						|
            self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION,
 | 
						|
                                                          #DIAN 1.7.-2020: 1.2.1.1
 | 
						|
                                                          rounding=decimal.ROUND_HALF_EVEN ))
 | 
						|
            self.currency = currency
 | 
						|
 | 
						|
    def fromNumber(self, val):
 | 
						|
        return Amount(val, currency=self.currency)
 | 
						|
    
 | 
						|
    def round(self, prec):
 | 
						|
        return Amount(round(self.amount, prec), currency=self.currency)
 | 
						|
 | 
						|
    def __round__(self, prec):
 | 
						|
        return round(self.amount, prec)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.float())
 | 
						|
 | 
						|
    def __lt__(self, other):
 | 
						|
        if not self.is_same_currency(other):
 | 
						|
            raise AmountCurrencyError()
 | 
						|
        return round(self.amount, DECIMAL_PRECISION) < round(other, 2)
 | 
						|
 | 
						|
    def __eq__(self, other):
 | 
						|
        if not self.is_same_currency(other):
 | 
						|
            raise AmountCurrencyError()
 | 
						|
        return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION)
 | 
						|
 | 
						|
    def _cast(self, val):
 | 
						|
        if type(val) in [int, float]:
 | 
						|
            return self.fromNumber(val)
 | 
						|
        if isinstance(val, Amount):
 | 
						|
            return val
 | 
						|
        raise TypeError("cant cast to amount")
 | 
						|
    
 | 
						|
    def __add__(self, rother):
 | 
						|
        other = self._cast(rother)
 | 
						|
        if not self.is_same_currency(other):
 | 
						|
            raise AmountCurrencyError()
 | 
						|
        return Amount(self.amount + other.amount, self.currency)
 | 
						|
 | 
						|
    def __sub__(self, rother):
 | 
						|
        other = self._cast(rother)
 | 
						|
        if not self.is_same_currency(other):
 | 
						|
            raise AmountCurrencyError()
 | 
						|
        return Amount(self.amount - other.amount, self.currency)
 | 
						|
 | 
						|
    def __mul__(self, rother):
 | 
						|
        other = self._cast(rother)
 | 
						|
        if not self.is_same_currency(other):
 | 
						|
            raise AmountCurrencyError()
 | 
						|
        return Amount(self.amount * other.amount, self.currency)
 | 
						|
 | 
						|
    def is_same_currency(self, other):
 | 
						|
        return self.currency == other.currency
 | 
						|
 | 
						|
    def truncate_as_string(self, prec):
 | 
						|
        parts = str(self.float()).split('.', 1)
 | 
						|
        return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0'))
 | 
						|
 | 
						|
    def float(self):
 | 
						|
        return float(round(self.amount, DECIMAL_PRECISION))
 | 
						|
    
 | 
						|
 | 
						|
class Quantity:
 | 
						|
    
 | 
						|
    def __init__(self, val, code):
 | 
						|
        if type(val) not in [float, int]:
 | 
						|
            raise ValueError('val expected int or float')
 | 
						|
        if code not in codelist.UnidadesMedida:
 | 
						|
            raise ValueError("code [%s] not found" % (code))
 | 
						|
 | 
						|
        self.value = Amount(val)
 | 
						|
        self.code = code
 | 
						|
 | 
						|
    def __mul__(self, other):
 | 
						|
        return self.value * other
 | 
						|
 | 
						|
    def __lt__(self, other):
 | 
						|
        return self.value < other
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.value)
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return str(self)
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Item:
 | 
						|
    scheme_name: str
 | 
						|
    scheme_agency_id: str
 | 
						|
    scheme_id: str
 | 
						|
    description: str
 | 
						|
    id: str
 | 
						|
 | 
						|
 | 
						|
class StandardItem(Item):
 | 
						|
    def __init__(self, id_: str, description: str = ''):
 | 
						|
        super().__init__(id=id_,
 | 
						|
                         description=description,
 | 
						|
                         scheme_name='',
 | 
						|
                         scheme_id='999',
 | 
						|
                         scheme_agency_id='')
 | 
						|
 | 
						|
 | 
						|
class UNSPSCItem(Item):
 | 
						|
    def __init__(self, id_: str, description: str = ''):
 | 
						|
        super().__init__(id=id_,
 | 
						|
                         description=description,
 | 
						|
                         scheme_name='UNSPSC',
 | 
						|
                         scheme_id='001',
 | 
						|
                         scheme_agency_id='10')        
 | 
						|
 | 
						|
        
 | 
						|
@dataclass
 | 
						|
class Country:
 | 
						|
    code: str
 | 
						|
    name: str = ''
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.Paises:
 | 
						|
            raise ValueError("code [%s] not found" % (self.code))
 | 
						|
        self.name = codelist.Paises[self.code]['name']
 | 
						|
 | 
						|
@dataclass
 | 
						|
class CountrySubentity:
 | 
						|
    code: str
 | 
						|
    name: str = ''
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.Departamento:
 | 
						|
            raise ValueError("code [%s] not found" % (self.code))
 | 
						|
        self.name = codelist.Departamento[self.code]['name']
 | 
						|
 | 
						|
@dataclass
 | 
						|
class City:
 | 
						|
    code: str
 | 
						|
    name: str = ''
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.Municipio:
 | 
						|
            raise ValueError("code [%s] not found" % (self.code))
 | 
						|
        self.name = codelist.Municipio[self.code]['name']
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Address:
 | 
						|
    name: str
 | 
						|
    street: str = ''
 | 
						|
    city: City = City('05001')
 | 
						|
    country: Country = Country('CO')
 | 
						|
    countrysubentity: CountrySubentity = CountrySubentity('05')
 | 
						|
 | 
						|
@dataclass
 | 
						|
class PartyIdentification:
 | 
						|
    number: str
 | 
						|
    dv: str
 | 
						|
    type_fiscal: str
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.number
 | 
						|
 | 
						|
    def __eq__(self, other):
 | 
						|
        return str(self) == str(other)
 | 
						|
 | 
						|
    def full(self):
 | 
						|
        return "%s%s" % [self.number, self.dv]
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.type_fiscal not in codelist.TipoIdFiscal:
 | 
						|
            raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal))
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Responsability:
 | 
						|
    codes: list
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return ';'.join(self.codes)
 | 
						|
 | 
						|
    def __eq__(self, other):
 | 
						|
        return str(self) == str(other)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        return iter(self.codes)
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        for code in self.codes:
 | 
						|
            if code not in codelist.TipoResponsabilidad:
 | 
						|
                raise ValueError("code %s not found" % (code))
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class TaxScheme:
 | 
						|
    code: str
 | 
						|
    name: str = ''
 | 
						|
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.TipoImpuesto:
 | 
						|
            raise ValueError("code not found")
 | 
						|
        self.name = codelist.TipoImpuesto[self.code]['name']
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Party:
 | 
						|
    name: str
 | 
						|
    ident: str
 | 
						|
    responsability_code: typing.List[Responsability]
 | 
						|
    responsability_regime_code: str
 | 
						|
    organization_code: str
 | 
						|
    tax_scheme: TaxScheme = TaxScheme('01')
 | 
						|
 | 
						|
    phone: str = ''
 | 
						|
    address: Address = Address('')
 | 
						|
    email: str = ''
 | 
						|
    legal_name: str = ''
 | 
						|
    legal_company_ident: str = ''
 | 
						|
    legal_address: str = ''
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.organization_code not in codelist.TipoOrganizacion:
 | 
						|
            raise ValueError("organization_code not found")
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class TaxScheme:
 | 
						|
    code: str = '01'
 | 
						|
 | 
						|
    @property
 | 
						|
    def name(self):
 | 
						|
        return codelist.TipoImpuesto[self.code]['name']
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.TipoImpuesto:
 | 
						|
            raise ValueError("code [%s] not found" % (self.code))
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class TaxSubTotal:
 | 
						|
    percent: float
 | 
						|
    scheme: typing.Optional[TaxScheme] = None
 | 
						|
    tax_amount: Amount = Amount(0.0)
 | 
						|
 | 
						|
    def calculate(self, invline):
 | 
						|
        if self.percent is not None:
 | 
						|
            self.tax_amount = invline.total_amount * Amount(self.percent / 100)
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class TaxTotal:
 | 
						|
    subtotals: list
 | 
						|
    tax_amount: Amount = Amount(0.0)
 | 
						|
    taxable_amount: Amount = Amount(0.0)
 | 
						|
 | 
						|
    def calculate(self, invline):
 | 
						|
        self.taxable_amount = invline.total_amount
 | 
						|
 | 
						|
        for subtax in self.subtotals:
 | 
						|
            subtax.calculate(invline)
 | 
						|
            self.tax_amount += subtax.tax_amount
 | 
						|
 | 
						|
 | 
						|
class TaxTotalOmit(TaxTotal):
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__([])
 | 
						|
 | 
						|
    def calculate(self, invline):
 | 
						|
        pass
 | 
						|
 | 
						|
@dataclass
 | 
						|
class Price:
 | 
						|
    amount: Amount
 | 
						|
    type_code: str
 | 
						|
    type: str
 | 
						|
    quantity: int = 1
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.type_code not in codelist.CodigoPrecioReferencia:
 | 
						|
            raise ValueError("type_code [%s] not found" % (self.type_code))
 | 
						|
        if not isinstance(self.quantity, int):
 | 
						|
            raise ValueError("quantity must be int")
 | 
						|
 | 
						|
        self.amount *= self.quantity
 | 
						|
 | 
						|
@dataclass
 | 
						|
class PaymentMean:
 | 
						|
    DEBIT = '01'
 | 
						|
    CREDIT = '02'
 | 
						|
 | 
						|
    def __init__(self, id: str, code: str, due_at: datetime, payment_id: str):
 | 
						|
        if code not in codelist.MediosPago:
 | 
						|
            raise ValueError("code not found")
 | 
						|
 | 
						|
        self.id = id
 | 
						|
        self.code = code
 | 
						|
        self.due_at = due_at
 | 
						|
        self.payment_id = payment_id
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class PrePaidPayment:
 | 
						|
    #DIAN 1.7.-2020: FBD03
 | 
						|
    paid_amount: Amount = Amount(0.0)
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class BillingReference:
 | 
						|
    ident: str
 | 
						|
    uuid: str
 | 
						|
    date: date
 | 
						|
 | 
						|
class CreditNoteDocumentReference(BillingReference):
 | 
						|
    """
 | 
						|
    ident: Prefijo + Numero de la factura relacionada
 | 
						|
    uuid: CUFE de la factura electronica
 | 
						|
    date: fecha de emision de la factura relacionada
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class DebitNoteDocumentReference(BillingReference):
 | 
						|
    """
 | 
						|
    ident: Prefijo + Numero de la factura relacionada
 | 
						|
    uuid: CUFE de la factura electronica
 | 
						|
    date: fecha de emision de la factura relacionada
 | 
						|
    """
 | 
						|
 | 
						|
class InvoiceDocumentReference(BillingReference):
 | 
						|
    """
 | 
						|
    ident: Prefijo + Numero de la nota credito relacionada
 | 
						|
    uuid: CUDE de la nota credito relacionada
 | 
						|
    date: fecha de emision de la nota credito relacionada
 | 
						|
    """
 | 
						|
 | 
						|
@dataclass
 | 
						|
class AllowanceChargeReason:
 | 
						|
    code: str
 | 
						|
    reason: str
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if self.code not in codelist.CodigoDescuento:
 | 
						|
            raise ValueError("code [%s] not found" % (self.code))
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class AllowanceCharge:
 | 
						|
    #DIAN 1.7.-2020: FAQ03
 | 
						|
    charge_indicator: bool = True
 | 
						|
    amount: Amount = Amount(0.0)
 | 
						|
    reason: AllowanceChargeReason = None
 | 
						|
 | 
						|
    #Valor Base para calcular el descuento o el cargo
 | 
						|
    base_amount: typing.Optional[Amount] = Amount(0.0)
 | 
						|
    
 | 
						|
    # Porcentaje: Porcentaje que aplicar.
 | 
						|
    multiplier_factor_numeric: Amount = Amount(1.0)
 | 
						|
    
 | 
						|
    def isCharge(self):
 | 
						|
        return self.charge_indicator == True
 | 
						|
 | 
						|
    def isDiscount(self):
 | 
						|
        return self.charge_indicator == False
 | 
						|
 | 
						|
    def asCharge(self):
 | 
						|
        self.charge_indicator = True
 | 
						|
 | 
						|
    def asDiscount(self):
 | 
						|
        self.charge_indicator = False
 | 
						|
 | 
						|
    def hasReason(self):
 | 
						|
        return self.reason is not None
 | 
						|
 | 
						|
    def set_base_amount(self, amount):
 | 
						|
        self.base_amount = amount
 | 
						|
 | 
						|
class AllowanceChargeAsDiscount(AllowanceCharge):
 | 
						|
    def __init__(self, amount: Amount = Amount(0.0)):
 | 
						|
        self.charge_indicator = False
 | 
						|
        self.amount = amount
 | 
						|
 | 
						|
@dataclass
 | 
						|
class InvoiceLine:
 | 
						|
    # RESOLUCION 0004: pagina 155
 | 
						|
    quantity: Quantity
 | 
						|
    description: str
 | 
						|
    item: Item
 | 
						|
    price: Price
 | 
						|
    # TODO mover a Invoice
 | 
						|
    # ya que al reportar los totales es sobre
 | 
						|
    # la factura y el percent es unico por type_code
 | 
						|
    # de subtotal
 | 
						|
    tax: typing.Optional[TaxTotal]
 | 
						|
 | 
						|
    allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list)
 | 
						|
 | 
						|
    def add_allowance_charge(self, charge):
 | 
						|
        if not isinstance(charge, AllowanceCharge):
 | 
						|
            raise TypeError('charge invalid type expected AllowanceCharge')
 | 
						|
        charge.set_base_amount(self.total_amount_without_charge)
 | 
						|
        self.allowance_charge.add(charge)
 | 
						|
 | 
						|
    @property
 | 
						|
    def total_amount_without_charge(self):
 | 
						|
        return (self.quantity * self.price.amount)
 | 
						|
    
 | 
						|
    @property
 | 
						|
    def total_amount(self):
 | 
						|
        charge = AmountCollection(self.allowance_charge)\
 | 
						|
            .filter(lambda charge: charge.isCharge())\
 | 
						|
            .map(lambda charge: charge.amount)\
 | 
						|
            .sum()
 | 
						|
 | 
						|
        discount = AmountCollection(self.allowance_charge)\
 | 
						|
            .filter(lambda charge: charge.isDiscount())\
 | 
						|
            .map(lambda charge: charge.amount)\
 | 
						|
            .sum()
 | 
						|
 | 
						|
        return self.total_amount_without_charge + charge - discount
 | 
						|
 | 
						|
    @property
 | 
						|
    def total_tax_inclusive_amount(self):
 | 
						|
        # FAU06
 | 
						|
        return self.total_amount + self.tax.tax_amount
 | 
						|
 | 
						|
    @property
 | 
						|
    def total_tax_exclusive_amount(self):
 | 
						|
        return self.tax.taxable_amount
 | 
						|
 | 
						|
    @property
 | 
						|
    def tax_amount(self):
 | 
						|
        return self.tax.tax_amount
 | 
						|
 | 
						|
    @property
 | 
						|
    def taxable_amount(self):
 | 
						|
        return self.tax.taxable_amount
 | 
						|
 | 
						|
    def calculate(self):
 | 
						|
        self.tax.calculate(self)
 | 
						|
 | 
						|
    def __post_init__(self):
 | 
						|
        if not isinstance(self.quantity, Quantity):
 | 
						|
            raise ValueError("quantity must be Amount")
 | 
						|
 | 
						|
        if self.tax is None:
 | 
						|
            self.tax = TaxTotalOmit()
 | 
						|
 | 
						|
@dataclass
 | 
						|
class LegalMonetaryTotal:
 | 
						|
    line_extension_amount: Amount = Amount(0.0)
 | 
						|
    tax_exclusive_amount: Amount = Amount(0.0)
 | 
						|
    tax_inclusive_amount: Amount = Amount(0.0)
 | 
						|
    charge_total_amount: Amount = Amount(0.0)
 | 
						|
    allowance_total_amount: Amount = Amount(0.0)
 | 
						|
    payable_amount: Amount = Amount(0.0)
 | 
						|
    prepaid_amount: Amount = Amount(0.0)
 | 
						|
 | 
						|
    def calculate(self):
 | 
						|
        #DIAN 1.7.-2020: FAU14
 | 
						|
        self.payable_amount = \
 | 
						|
            self.tax_inclusive_amount \
 | 
						|
            + self.allowance_total_amount \
 | 
						|
            + self.charge_total_amount \
 | 
						|
            - self.prepaid_amount
 | 
						|
 | 
						|
 | 
						|
 | 
						|
class NationalSalesInvoiceDocumentType(str):
 | 
						|
    def __str__(self):
 | 
						|
        # 6.1.3
 | 
						|
        return '01'
 | 
						|
 | 
						|
class CreditNoteDocumentType(str):
 | 
						|
    def __str__(self):
 | 
						|
        # 6.1.3
 | 
						|
        return '91'
 | 
						|
 | 
						|
class DebitNoteDocumentType(str):
 | 
						|
    def __str__(self):
 | 
						|
        # 6.1.3
 | 
						|
        return '92'
 | 
						|
 | 
						|
class Invoice:
 | 
						|
    def __init__(self, type_code: str):
 | 
						|
        if str(type_code) not in codelist.TipoDocumento:
 | 
						|
            raise ValueError("type_code [%s] not found")
 | 
						|
 | 
						|
        self.invoice_period_start = None
 | 
						|
        self.invoice_period_end = None
 | 
						|
        self.invoice_issue = None
 | 
						|
        self.invoice_ident = None
 | 
						|
        self.invoice_operation_type = None
 | 
						|
        self.invoice_legal_monetary_total = LegalMonetaryTotal()
 | 
						|
        self.invoice_customer = None
 | 
						|
        self.invoice_supplier = None
 | 
						|
        self.invoice_payment_mean = None
 | 
						|
        self.invoice_payments = []
 | 
						|
        self.invoice_lines = []
 | 
						|
        self.invoice_allowance_charge = []
 | 
						|
        self.invoice_prepaid_payment = []
 | 
						|
        self.invoice_billing_reference = None
 | 
						|
        self.invoice_type_code = str(type_code)
 | 
						|
        self.invoice_ident_prefix = None
 | 
						|
 | 
						|
    def set_period(self, startdate, enddate):
 | 
						|
        self.invoice_period_start = startdate
 | 
						|
        self.invoice_period_end = enddate
 | 
						|
 | 
						|
    def set_issue(self, dtime: datetime):
 | 
						|
        if dtime.tzname() not in ['UTC-05:00', '-05', None]:
 | 
						|
            raise ValueError("dtime must be UTC-05:00")
 | 
						|
 | 
						|
        self.invoice_issue = dtime
 | 
						|
 | 
						|
    def _set_ident_prefix_automatic(self):
 | 
						|
        if not self.invoice_ident_prefix:
 | 
						|
            prefix = ''
 | 
						|
            for idx, val in enumerate(self.invoice_ident):
 | 
						|
                if val.isalpha():
 | 
						|
                    prefix += val
 | 
						|
                else:
 | 
						|
                    break
 | 
						|
 | 
						|
            if len(prefix) <= 4:
 | 
						|
                self.invoice_ident_prefix = prefix
 | 
						|
            else:
 | 
						|
                raise ValueError('ident prefix failed to get, expected 0  to 4 chars')
 | 
						|
 | 
						|
    def set_ident(self, ident: str):
 | 
						|
        """
 | 
						|
        identificador de factura; prefijo + consecutivo
 | 
						|
        """
 | 
						|
        self.invoice_ident = ident
 | 
						|
        self._set_ident_prefix_automatic()
 | 
						|
 | 
						|
    def _check_ident_prefix(self, prefix):
 | 
						|
        if len(prefix) != 4:
 | 
						|
            raise ValueError('prefix must be 4 length')
 | 
						|
 | 
						|
    def set_ident_prefix(self, prefix: str):
 | 
						|
        """
 | 
						|
        prefijo de facturacion: ejemplo SETP
 | 
						|
        """
 | 
						|
        self._check_ident_prefix(prefix)
 | 
						|
        self.invoice_ident_prefix = prefix
 | 
						|
 | 
						|
    def set_supplier(self, party: Party):
 | 
						|
        self.invoice_supplier = party
 | 
						|
 | 
						|
    def set_customer(self, party: Party):
 | 
						|
        self.invoice_customer = party
 | 
						|
 | 
						|
    def set_payment_mean(self, payment_mean: PaymentMean):
 | 
						|
        self.invoice_payment_mean = payment_mean
 | 
						|
 | 
						|
    def _get_codelist_tipo_operacion(self):
 | 
						|
        return codelist.TipoOperacionF
 | 
						|
    
 | 
						|
    def set_operation_type(self, operation):
 | 
						|
        if operation not in self._get_codelist_tipo_operacion():
 | 
						|
            raise ValueError("operation not found")
 | 
						|
 | 
						|
        self.invoice_operation_type = operation
 | 
						|
 | 
						|
    def add_allowance_charge(self, charge: AllowanceCharge):
 | 
						|
        self.invoice_allowance_charge.append(charge)
 | 
						|
 | 
						|
    def add_invoice_line(self, line: InvoiceLine):
 | 
						|
        self.invoice_lines.append(line)
 | 
						|
 | 
						|
    def add_prepaid_payment(self, paid: PrePaidPayment):
 | 
						|
        self.invoice_prepaid_payment.append(paid)
 | 
						|
 | 
						|
    def set_billing_reference(self, billing_reference: BillingReference):
 | 
						|
        self.invoice_billing_reference = billing_reference
 | 
						|
 | 
						|
    def accept(self, visitor):
 | 
						|
        visitor.visit_payment_mean(self.invoice_payment_mean)
 | 
						|
        visitor.visit_customer(self.invoice_customer)
 | 
						|
        visitor.visit_supplier(self.invoice_supplier)
 | 
						|
        for payment in self.invoice_payments:
 | 
						|
            visitor.visit_payment(payment)
 | 
						|
        for invline in self.invoice_lines:
 | 
						|
            visitor.visit_invoice_line(invline)
 | 
						|
 | 
						|
    def _calculate_legal_monetary_total(self):
 | 
						|
        for invline in self.invoice_lines:
 | 
						|
            self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount
 | 
						|
            self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount
 | 
						|
            #DIAN 1.7.-2020: FAU6
 | 
						|
            self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount
 | 
						|
 | 
						|
        #DIAN 1.7.-2020: FAU08
 | 
						|
        self.invoice_legal_monetary_total.allowance_total_amount = AmountCollection(self.invoice_allowance_charge)\
 | 
						|
            .filter(lambda charge: charge.isDiscount())\
 | 
						|
            .map(lambda charge: charge.amount)\
 | 
						|
            .sum()
 | 
						|
 | 
						|
        #DIAN 1.7.-2020: FAU10
 | 
						|
        self.invoice_legal_monetary_total.charge_total_amount = AmountCollection(self.invoice_allowance_charge)\
 | 
						|
            .filter(lambda charge: charge.isCharge())\
 | 
						|
            .map(lambda charge: charge.amount)\
 | 
						|
            .sum()
 | 
						|
 | 
						|
        #DIAN 1.7.-2020: FAU12
 | 
						|
        self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\
 | 
						|
            .map(lambda paid: paid.paid_amount)\
 | 
						|
            .sum()
 | 
						|
 | 
						|
        #DIAN 1.7.-2020: FAU14
 | 
						|
        self.invoice_legal_monetary_total.calculate()
 | 
						|
 | 
						|
    def _refresh_charges_base_amount(self):
 | 
						|
        if self.invoice_allowance_charge:
 | 
						|
            for invline in self.invoice_lines:
 | 
						|
                if invline.allowance_charge:
 | 
						|
                    # TODO actualmente solo uno de los cargos es permitido
 | 
						|
                    raise ValueError('allowance charge in invoice exclude invoice line')
 | 
						|
            
 | 
						|
        # cargos a nivel de factura
 | 
						|
        for charge in self.invoice_allowance_charge:
 | 
						|
            charge.set_base_amount(self.invoice_legal_monetary_total.line_extension_amount)
 | 
						|
        
 | 
						|
    def calculate(self):
 | 
						|
        for invline in self.invoice_lines:
 | 
						|
            invline.calculate()
 | 
						|
        self._calculate_legal_monetary_total()
 | 
						|
        self._refresh_charges_base_amount()
 | 
						|
 | 
						|
class NationalSalesInvoice(Invoice):
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__(NationalSalesInvoiceDocumentType())
 | 
						|
 | 
						|
 | 
						|
class CreditNote(Invoice):
 | 
						|
    def __init__(self, invoice_document_reference: BillingReference):
 | 
						|
        super().__init__(CreditNoteDocumentType())
 | 
						|
 | 
						|
        if not isinstance(invoice_document_reference, BillingReference):
 | 
						|
            raise TypeError('invoice_document_reference invalid type')
 | 
						|
        self.invoice_billing_reference = invoice_document_reference
 | 
						|
 | 
						|
    def _get_codelist_tipo_operacion(self):
 | 
						|
        return codelist.TipoOperacionNC
 | 
						|
    
 | 
						|
    def _check_ident_prefix(self, prefix):
 | 
						|
        if len(prefix) != 6:
 | 
						|
            raise ValueError('prefix must be 6 length')
 | 
						|
 | 
						|
    def _set_ident_prefix_automatic(self):
 | 
						|
        if not self.invoice_ident_prefix:
 | 
						|
            self.invoice_ident_prefix = self.invoice_ident[0:6]
 | 
						|
 | 
						|
 | 
						|
class DebitNote(Invoice):
 | 
						|
    def __init__(self, invoice_document_reference: BillingReference):
 | 
						|
        super().__init__(DebitNoteDocumentType())
 | 
						|
 | 
						|
        if not isinstance(invoice_document_reference, BillingReference):
 | 
						|
            raise TypeError('invoice_document_reference invalid type')
 | 
						|
        self.invoice_billing_reference = invoice_document_reference
 | 
						|
 | 
						|
    def _get_codelist_tipo_operacion(self):
 | 
						|
        return codelist.TipoOperacionND
 | 
						|
 | 
						|
    def _check_ident_prefix(self, prefix):
 | 
						|
        if len(prefix) != 6:
 | 
						|
            raise ValueError('prefix must be 6 length')
 | 
						|
 | 
						|
    def _set_ident_prefix_automatic(self):
 | 
						|
        if not self.invoice_ident_prefix:
 | 
						|
            self.invoice_ident_prefix = self.invoice_ident[0:6]
 | 
						|
 |