oc-facho/facho/fe/form.py
alnus@disroot.org 3a7ccd774b Notas y Validaciones
FossilOrigin-Name: ef048c9ce72962e7f134b9c2fc52544bf25a1232e8f0c56a9933de22473e9674
2020-10-26 02:06:52 +00:00

480 lines
13 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
from dataclasses import dataclass
from datetime import datetime
from collections import defaultdict
import decimal
from decimal import Decimal
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 Amount, currency: Currency = Currency('COP')):
if isinstance(amount, Amount):
self.amount = amount.amount
self.currency = amount.currency
else:
self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, rounding=decimal.ROUND_HALF_DOWN ))
self.currency = currency
def __round__(self, prec):
return round(self.amount, prec)
def __str__(self):
return '%.06f' % self.amount
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 __add__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return Amount(self.amount + other.amount, self.currency)
def __sub__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return Amount(self.amount - other.amount, self.currency)
def __mul__(self, other):
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
@dataclass
class Item:
description: str
id: str
@dataclass
class StandardItem(Item):
pass
@dataclass
class Country:
code: str
name: str
@dataclass
class CountrySubentity:
code: str
name: str
@dataclass
class City:
code: str
name: str
@dataclass
class Address:
name: str
street: str = ''
city: City = City('', '')
country: Country = Country('CO', 'Colombia')
countrysubentity: CountrySubentity = CountrySubentity('', '')
@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]
@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)
@dataclass
class TaxScheme:
code: str
name: str = ''
@dataclass
class Party:
name: str
ident: str
responsability_code: str
responsability_regime_code: str
organization_code: str
tax_scheme: TaxScheme = TaxScheme('')
phone: str = ''
address: Address = Address('')
email: str = ''
legal_name: str = ''
legal_company_ident: str = ''
legal_address: str = ''
@dataclass
class TaxSubTotal:
percent: float
tax_scheme_ident: str = '01'
tax_scheme_name: str = 'IVA'
tax_amount: Amount = Amount(0.0)
taxable_amount: Amount = Amount(0.0)
def calculate(self, invline):
self.tax_amount = invline.total_amount * Amount(self.percent / 100)
self.taxable_amount = invline.total_amount
@dataclass
class TaxTotal:
subtotals: list
tax_amount: Amount = Amount(0.0)
taxable_amount: Amount = Amount(0.0)
def calculate(self, invline):
for subtax in self.subtotals:
subtax.calculate(invline)
self.tax_amount += subtax.tax_amount
self.taxable_amount += subtax.taxable_amount
@dataclass
class Price:
amount: Amount
type_code: str
type: str
@dataclass
class PaymentMean:
DEBIT = '01'
CREDIT = '02'
def __init__(self, id: str, code: str, due_at: datetime, payment_id: str):
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:
def __init__(self, ident: str, uuid: str, date: str):
self.ident = ident
self.uuid = uuid
self.date = date
@dataclass
class InvoiceLine:
# RESOLUCION 0004: pagina 155
quantity: int
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: TaxTotal
@property
def total_amount(self):
return Amount(self.quantity) * self.price.amount
@property
def total_tax_inclusive_amount(self):
return self.tax.taxable_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)
@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)
@dataclass
class AllowanceCharge:
#DIAN 1.7.-2020: FAQ03
charge_indicator: bool = True
amount: Amount = Amount(0.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
class Invoice:
def __init__(self):
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 = []
def set_period(self, startdate, enddate):
self.invoice_period_start = startdate
self.invoice_period_end = enddate
def set_issue(self, dtime: datetime):
self.invoice_issue = dtime
def set_ident(self, ident: str):
self.invoice_ident = ident
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 set_operation_type(self, operation):
self.invoice_operation_type = operation
def add_allownace_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.payable_amount = \
self.invoice_legal_monetary_total.tax_inclusive_amount \
+ self.invoice_legal_monetary_total.allowance_total_amount \
+ self.invoice_legal_monetary_total.charge_total_amount \
- self.invoice_legal_monetary_total.prepaid_amount
def calculate(self):
for invline in self.invoice_lines:
invline.calculate()
self._calculate_legal_monetary_total()
class DianResolucion0001Validator:
def __init__(self):
self.errors = []
def _validate_party(self, model, party):
for code in party.responsability_code:
if code not in codelist.TipoResponsabilidad:
self.errors.append((model,
'responsability_code',
'not found %s' % (code)))
try:
codelist.TipoOrganizacion[party.organization_code]
except KeyError:
self.errors.append((model, 'organization_code' ,
'not found %s' % (party.organization_code)))
try:
if isinstance(party.tax_scheme, (str, str)):
codelist.TipoImpuesto[party.tax_scheme.code]
except KeyError:
self.errors.append((model , 'tax_scheme' ,
'not found %s' % (party.tax_scheme)))
try:
codelist.Departamento[party.address.countrysubentity.code]
except KeyError:
self.errors.append((model, 'countrysubentity_code',
'not found %s' % (party.address.countrysubentity.code)))
try:
codelist.Municipio[party.address.city.code]
except KeyError:
self.errors.append((model, 'city_code',
'not found %s' % (party.address.city.code)))
def _validate_invoice(self, invoice):
try:
codelist.TipoOperacionF[invoice.invoice_operation_type]
except KeyError:
self.errors.append(('invoice', 'operation_type',
'not found %s' % (invoice.invoice_operation_type)))
# MACHETE se espera en zona horario colombia
if invoice.invoice_issue.tzname() not in ['UTC-05:00', '-05', None]:
self.errors.append(('invoice', 'invoice_issue',
'expected timezone UTC-05:00 or -05 or empty got %s' % (invoice.invoice_issue.tzname())))
def validate(self, invoice):
invoice.accept(self)
self._validate_invoice(invoice)
return not self.errors
def visit_payment_mean(self, mean):
try:
codelist.MediosPago[mean.code]
except KeyError:
self.errors.append(('payment_mean', 'code',
'not found %s' % (mean.code)))
def visit_customer(self, customer):
self._validate_party('customer', customer)
def visit_supplier(self, supplier):
self._validate_party('supplier', supplier)
def visit_payment(self, payment):
pass
def visit_invoice_line(self, line):
try:
codelist.CodigoPrecioReferencia[line.price.type_code]
except KeyError:
self.errors.append(('invoice_line', 'line.price',
'not found %s' % (line.price.type_code)))
def valid(self):
return not self.errors