eo_facture: manage invoice cancelation (#36633)
This commit is contained in:
parent
53a671300c
commit
077ebf0ecc
|
@ -386,6 +386,11 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
self.admin_site.admin_view(self.sheet),
|
||||
name="eo_facture_facture_sheet",
|
||||
),
|
||||
url(
|
||||
r'^(.+)/cancel/',
|
||||
self.admin_site.admin_view(views.cancel),
|
||||
name='eo_facture_facture_cancel',
|
||||
),
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
|
|
|
@ -228,6 +228,9 @@ class Facture(models.Model):
|
|||
contrat = models.ForeignKey(
|
||||
Contrat, related_name='factures', blank=True, null=True, on_delete=models.CASCADE
|
||||
)
|
||||
annulation = models.ForeignKey(
|
||||
'Facture', related_name='factures_avoir', blank=True, null=True, on_delete=models.CASCADE
|
||||
)
|
||||
intitule = models.CharField(max_length=150, verbose_name='Intitulé', blank=True)
|
||||
notes = models.TextField(blank=True)
|
||||
emission = models.DateField(verbose_name="Émission", default=today, db_index=True)
|
||||
|
@ -379,7 +382,7 @@ class Facture(models.Model):
|
|||
raise RuntimeError('facturx_pdf() can only be called on non proforma invoices')
|
||||
facturx_ctx = {
|
||||
'numero_de_facture': self.code(),
|
||||
'type_facture': '380',
|
||||
'type_facture': '380' if not self.annulation else '381', # 380 => facture, 381 => avoir
|
||||
'date_emission': self.emission,
|
||||
'vendeur': settings.VENDOR_NAME,
|
||||
'vendeur_siret': settings.VENDOR_SIRET,
|
||||
|
@ -393,6 +396,7 @@ class Facture(models.Model):
|
|||
'montant_ttc': str(self.montant_ttc),
|
||||
'montant_tva': str(self.tva),
|
||||
'taux_tva': self.taux_tva or (self.contrat and self.contrat.tva) or self.client.tva or 0,
|
||||
'annulation_code': self.annulation.code() if self.annulation else None,
|
||||
}
|
||||
return facturx.add_facturx_from_bytes(
|
||||
self.pdf(template_name=template_name, base_uri=base_uri), facturx_ctx
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
{% if original.client.chorus_structure and not original.proforma %}
|
||||
<li><a href="{% url "admin:eo_facture_facture_send_to_chorus" original.id %}">Envoyer à Chorus</a></li>
|
||||
{% endif %}
|
||||
{% if not original.annulation %}
|
||||
<li><a href="{% url "admin:eo_facture_facture_cancel" original.id %}">Annuler</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -78,6 +78,16 @@ CHORUS PRO : pour le secteur public, il s'agit du "Service Executant". Il est ob
|
|||
<ram:ApplicableHeaderTradeDelivery/>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
||||
{% if annulation_code %}
|
||||
{% comment %}
|
||||
Value "Clearing between partners" explained on S2.20, page 171 of
|
||||
the Chorus specifications: https://dev.entrouvert.org/attachments/60076
|
||||
Default value is "virement" for Chorus (and we guess for Factur-X too)
|
||||
{% endcomment %}
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>97</ram:TypeCode>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
{% endif %}
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>{{ montant_tva }}</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
|
@ -92,6 +102,15 @@ CHORUS PRO : pour le secteur public, il s'agit du "Service Executant". Il est ob
|
|||
<ram:GrandTotalAmount>{{ montant_ttc }}</ram:GrandTotalAmount>
|
||||
<ram:DuePayableAmount>{{ montant_ttc }}</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
{% if annulation_code %}
|
||||
{% comment %}
|
||||
"identifiant de la facture d’origine" explained on G1.05, page 74 and 113 of
|
||||
the Chorus specifications: https://dev.entrouvert.org/attachments/60076
|
||||
{% endcomment %}
|
||||
<ram:InvoiceReferencedDocument>
|
||||
<ram:IssuerAssignedID>{{ annulation_code }}</ram:IssuerAssignedID>
|
||||
</ram:InvoiceReferencedDocument>
|
||||
{% endif %}
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>{% endlocalize %}
|
||||
|
|
|
@ -115,3 +115,26 @@ def send_to_chorus(request, facture_id):
|
|||
logger.warning('failed to send invoice %s to ChorusPro, response: %s', facture, description)
|
||||
messages.error(request, msg)
|
||||
return redirect('admin:eo_facture_facture_change', facture_id)
|
||||
|
||||
|
||||
def cancel(request, facture_id):
|
||||
facture = get_object_or_404(Facture, id=facture_id)
|
||||
facture_avoir = get_object_or_404(Facture, id=facture_id)
|
||||
facture_avoir.pk = None
|
||||
facture_avoir.ordre = None
|
||||
facture_avoir.annulation = facture
|
||||
facture_avoir.proforma = True
|
||||
facture_avoir.intitule = "AVOIR POUR LA FACTURE " + facture.intitule
|
||||
facture_avoir.paid = False
|
||||
facture_avoir.creator = request.user
|
||||
facture_avoir.account_on_previous_period = False
|
||||
facture_avoir.save()
|
||||
for ligne in facture.lignes.all():
|
||||
ligne.pk = None
|
||||
ligne.facture = facture_avoir
|
||||
ligne.quantite = ligne.quantite * -1
|
||||
ligne.save()
|
||||
for payment in facture.payments.all():
|
||||
payment.pk = None
|
||||
payment.save()
|
||||
return redirect('admin:eo_facture_facture_change', facture_avoir.id)
|
||||
|
|
|
@ -30,3 +30,8 @@ DATABASES = {
|
|||
for key in ('PGPORT', 'PGHOST', 'PGUSER', 'PGPASSWORD'):
|
||||
if key in os.environ:
|
||||
DATABASES['default']['TEST'][key[2:]] = os.environ[key]
|
||||
|
||||
|
||||
VENDOR_NAME = "Entr'ouvert"
|
||||
VENDOR_SIRET = '44317013900036'
|
||||
VENDOR_TVAI = 'FR09491081899'
|
||||
|
|
|
@ -122,3 +122,96 @@ def test_add_facturx_from_bytes(fake_invoice_bytes):
|
|||
],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def test_add_facturx_from_bytes_facture_avoir(fake_invoice_bytes):
|
||||
facturx_ctx = {
|
||||
'numero_de_facture': 'F20190002',
|
||||
'type_facture': '381',
|
||||
'date_emission': datetime.date(2019, 1, 1),
|
||||
'chorus_service_code': 'service-code',
|
||||
'vendeur': 'Entr\'ouvert',
|
||||
'vendeur_siret': '44317013900036',
|
||||
'vendeur_tvai': 'FR09491081899',
|
||||
'client_siret': '1234',
|
||||
'client_name': 'RGFIPD',
|
||||
'numero_engagement': '5678',
|
||||
'numero_marche': 'ABCD',
|
||||
'montant_ht': '10',
|
||||
'montant_ttc': '12',
|
||||
'montant_tva': '2',
|
||||
'taux_tva': 20.0,
|
||||
'annulation_code': 'F20190001',
|
||||
}
|
||||
facturx_bytes = add_facturx_from_bytes(fake_invoice_bytes, facturx_ctx)
|
||||
_, xml_str = facturx.get_facturx_xml_from_pdf(io.BytesIO(facturx_bytes))
|
||||
root = ET.fromstring(xml_str)
|
||||
|
||||
def helper(root):
|
||||
tag = root.tag.split('}')[-1]
|
||||
if len(root) == 0:
|
||||
return [tag, root.text or '']
|
||||
else:
|
||||
return [tag] + [helper(node) for node in root]
|
||||
|
||||
assert helper(root) == [
|
||||
'CrossIndustryInvoice',
|
||||
[
|
||||
'ExchangedDocumentContext',
|
||||
['BusinessProcessSpecifiedDocumentContextParameter', ['ID', 'A1']],
|
||||
['GuidelineSpecifiedDocumentContextParameter', ['ID', 'urn:factur-x.eu:1p0:basicwl']],
|
||||
],
|
||||
[
|
||||
'ExchangedDocument',
|
||||
['ID', 'F20190002'],
|
||||
['TypeCode', '381'],
|
||||
['IssueDateTime', ['DateTimeString', '20190101']],
|
||||
],
|
||||
[
|
||||
'SupplyChainTradeTransaction',
|
||||
[
|
||||
'ApplicableHeaderTradeAgreement',
|
||||
['BuyerReference', 'service-code'],
|
||||
[
|
||||
'SellerTradeParty',
|
||||
['Name', "Entr'ouvert"],
|
||||
['SpecifiedLegalOrganization', ['ID', '44317013900036']],
|
||||
['PostalTradeAddress', ['CountryID', 'FR']],
|
||||
['SpecifiedTaxRegistration', ['ID', 'FR09491081899']],
|
||||
],
|
||||
[
|
||||
'BuyerTradeParty',
|
||||
['Name', 'RGFIPD'],
|
||||
[
|
||||
'SpecifiedLegalOrganization',
|
||||
['ID', '1234'],
|
||||
],
|
||||
],
|
||||
['BuyerOrderReferencedDocument', ['IssuerAssignedID', '5678']],
|
||||
['ContractReferencedDocument', ['IssuerAssignedID', 'ABCD']],
|
||||
],
|
||||
['ApplicableHeaderTradeDelivery', ''],
|
||||
[
|
||||
'ApplicableHeaderTradeSettlement',
|
||||
['InvoiceCurrencyCode', 'EUR'],
|
||||
['SpecifiedTradeSettlementPaymentMeans', ['TypeCode', '97']],
|
||||
[
|
||||
'ApplicableTradeTax',
|
||||
['CalculatedAmount', '2'],
|
||||
['TypeCode', 'VAT'],
|
||||
['BasisAmount', '10'],
|
||||
['CategoryCode', 'S'],
|
||||
['RateApplicablePercent', '20.0'],
|
||||
],
|
||||
[
|
||||
'SpecifiedTradeSettlementHeaderMonetarySummation',
|
||||
['LineTotalAmount', '10'],
|
||||
['TaxBasisTotalAmount', '10'],
|
||||
['TaxTotalAmount', '2'],
|
||||
['GrandTotalAmount', '12'],
|
||||
['DuePayableAmount', '12'],
|
||||
],
|
||||
['InvoiceReferencedDocument', ['IssuerAssignedID', 'F20190001']],
|
||||
],
|
||||
],
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue