feat: improve invoicing UX and supplier visibility
This commit is contained in:
+61
-2
@@ -29,6 +29,13 @@
|
||||
"sent_at",
|
||||
"last_sync_at",
|
||||
"inbound_section",
|
||||
"supplier_summary_html",
|
||||
"supplier_identity_details_section",
|
||||
"remote_supplier_name",
|
||||
"remote_supplier_vat_id",
|
||||
"remote_supplier_column",
|
||||
"remote_supplier_electronic_scheme",
|
||||
"remote_supplier_electronic_value",
|
||||
"review_status",
|
||||
"supplier",
|
||||
"supplier_match_method",
|
||||
@@ -254,6 +261,55 @@
|
||||
"fieldtype": "Section Break",
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_summary_html",
|
||||
"label": "Résumé du fournisseur",
|
||||
"fieldtype": "HTML",
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_identity_details_section",
|
||||
"label": "Données d’identification reçues",
|
||||
"fieldtype": "Section Break",
|
||||
"depends_on": "eval:doc.direction == 'Inbound'",
|
||||
"collapsible": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_supplier_name",
|
||||
"label": "Émetteur reçu",
|
||||
"fieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_supplier_vat_id",
|
||||
"label": "N° TVA reçu",
|
||||
"fieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"in_standard_filter": 1,
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_supplier_column",
|
||||
"fieldtype": "Column Break",
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_supplier_electronic_scheme",
|
||||
"label": "Schéma d’adresse électronique",
|
||||
"fieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_supplier_electronic_value",
|
||||
"label": "Adresse électronique reçue",
|
||||
"fieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"depends_on": "eval:doc.direction == 'Inbound'"
|
||||
},
|
||||
{
|
||||
"fieldname": "review_status",
|
||||
"label": "Statut d’examen",
|
||||
@@ -269,13 +325,16 @@
|
||||
"label": "Fournisseur rapproché",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_match_method",
|
||||
"label": "Méthode de rapprochement",
|
||||
"fieldtype": "Data",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"in_standard_filter": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "remote_invoice_number",
|
||||
|
||||
+15
-3
@@ -49,9 +49,21 @@ class ElectronicInvoiceExchange(Document):
|
||||
or _("Facture fournisseur")
|
||||
)
|
||||
|
||||
self.display_title = _(
|
||||
"Facture fournisseur {0}"
|
||||
).format(invoice_number)
|
||||
supplier_name = str(
|
||||
self.remote_supplier_name
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
self.display_title = (
|
||||
_("{0} — {1}").format(
|
||||
supplier_name,
|
||||
invoice_number,
|
||||
)
|
||||
if supplier_name
|
||||
else _(
|
||||
"Facture fournisseur {0}"
|
||||
).format(invoice_number)
|
||||
)
|
||||
|
||||
if not self.review_status:
|
||||
self.review_status = "To Review"
|
||||
|
||||
@@ -265,6 +265,9 @@ doctype_js = {
|
||||
"Customer": "public/js/customer.js",
|
||||
"Sales Invoice": "public/js/sales_invoice.js",
|
||||
"Purchase Invoice": "public/js/purchase_invoice.js",
|
||||
"Electronic Invoice Exchange": (
|
||||
"public/js/electronic_invoice_exchange.js"
|
||||
),
|
||||
}
|
||||
|
||||
# ERPNext document validation
|
||||
@@ -298,6 +301,9 @@ doctype_list_js = {
|
||||
"Electronic Invoice Exchange": (
|
||||
"public/js/electronic_invoice_exchange_list.js"
|
||||
),
|
||||
"Sales Invoice": (
|
||||
"public/js/sales_invoice_list.js"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -251,6 +251,27 @@ def build_inbound_exchange_values(
|
||||
"party": supplier or "",
|
||||
"supplier": supplier or "",
|
||||
"supplier_match_method": match_method,
|
||||
"remote_supplier_name": str(
|
||||
seller.get("name")
|
||||
or seller.get("trading_name")
|
||||
or ""
|
||||
).strip(),
|
||||
"remote_supplier_vat_id": str(
|
||||
seller.get("vat_identifier")
|
||||
or ""
|
||||
).strip(),
|
||||
"remote_supplier_electronic_scheme": str(
|
||||
seller.get(
|
||||
"electronic_address_scheme"
|
||||
)
|
||||
or ""
|
||||
).strip(),
|
||||
"remote_supplier_electronic_value": str(
|
||||
seller.get(
|
||||
"electronic_address_value"
|
||||
)
|
||||
or ""
|
||||
).strip(),
|
||||
"review_status": (
|
||||
"Supplier Matched"
|
||||
if supplier
|
||||
@@ -598,6 +619,91 @@ def clear_supplier_match(
|
||||
}
|
||||
|
||||
|
||||
def backfill_inbound_supplier_identity() -> int:
|
||||
"""Matérialiser l’identité des émetteurs déjà archivés."""
|
||||
|
||||
exchange_names = frappe.get_all(
|
||||
"Electronic Invoice Exchange",
|
||||
filters={
|
||||
"direction": "Inbound",
|
||||
},
|
||||
pluck="name",
|
||||
limit_page_length=0,
|
||||
)
|
||||
|
||||
updated = 0
|
||||
|
||||
for exchange_name in exchange_names:
|
||||
exchange = frappe.get_doc(
|
||||
"Electronic Invoice Exchange",
|
||||
exchange_name,
|
||||
)
|
||||
|
||||
seller = _get_seller_snapshot(
|
||||
exchange
|
||||
)
|
||||
|
||||
supplier_name = str(
|
||||
seller.get("name")
|
||||
or seller.get("trading_name")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
invoice_number = str(
|
||||
exchange.remote_invoice_number
|
||||
or exchange.remote_id
|
||||
or exchange.external_id
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
display_title = (
|
||||
f"{supplier_name} — {invoice_number}"
|
||||
if supplier_name
|
||||
else f"Facture fournisseur {invoice_number}"
|
||||
)
|
||||
|
||||
values = {
|
||||
"remote_supplier_name": supplier_name,
|
||||
"remote_supplier_vat_id": str(
|
||||
seller.get("vat_identifier")
|
||||
or ""
|
||||
).strip(),
|
||||
"remote_supplier_electronic_scheme": str(
|
||||
seller.get(
|
||||
"electronic_address_scheme"
|
||||
)
|
||||
or ""
|
||||
).strip(),
|
||||
"remote_supplier_electronic_value": str(
|
||||
seller.get(
|
||||
"electronic_address_value"
|
||||
)
|
||||
or ""
|
||||
).strip(),
|
||||
"display_title": display_title,
|
||||
}
|
||||
|
||||
changed = any(
|
||||
str(exchange.get(fieldname) or "")
|
||||
!= str(value or "")
|
||||
for fieldname, value in values.items()
|
||||
)
|
||||
|
||||
if not changed:
|
||||
continue
|
||||
|
||||
frappe.db.set_value(
|
||||
"Electronic Invoice Exchange",
|
||||
exchange.name,
|
||||
values,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
updated += 1
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
def archive_incoming_invoice(
|
||||
company: str,
|
||||
invoice_id: int,
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
// Copyright (c) 2026, Enuxia and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Electronic Invoice Exchange", {
|
||||
refresh(frm) {
|
||||
if (frm.doc.direction !== "Inbound") {
|
||||
return;
|
||||
}
|
||||
|
||||
render_supplier_summary(frm);
|
||||
add_supplier_actions(frm);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function escape_value(value) {
|
||||
return frappe.utils.escape_html(
|
||||
String(value || "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function display_value(value) {
|
||||
const normalized = String(
|
||||
value || ""
|
||||
).trim();
|
||||
|
||||
return normalized
|
||||
? escape_value(normalized)
|
||||
: `<span class="text-muted">${__("Non renseigné")}</span>`;
|
||||
}
|
||||
|
||||
|
||||
function supplier_link(supplier) {
|
||||
if (!supplier) {
|
||||
return `
|
||||
<span class="text-muted">
|
||||
${__("Aucun fournisseur associé")}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
const route = frappe.utils.get_form_link(
|
||||
"Supplier",
|
||||
supplier
|
||||
);
|
||||
|
||||
return `
|
||||
<a href="${route}">
|
||||
${escape_value(supplier)}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function get_match_label(method) {
|
||||
const labels = {
|
||||
"Exact VAT ID": __("TVA exacte"),
|
||||
"Normalized VAT ID": __("TVA normalisée"),
|
||||
"Manual selection – VAT confirmed": __(
|
||||
"Sélection manuelle — TVA confirmée"
|
||||
),
|
||||
"Manual selection": __("Sélection manuelle"),
|
||||
};
|
||||
|
||||
return labels[method] || method || __("Aucune");
|
||||
}
|
||||
|
||||
|
||||
function get_review_label(status) {
|
||||
const labels = {
|
||||
"To Review": __("À vérifier"),
|
||||
"Supplier Matched": __("Fournisseur identifié"),
|
||||
"Ready for Import": __("Prête pour l’import"),
|
||||
"Imported": __("Importée"),
|
||||
"Ignored": __("Ignorée"),
|
||||
"Error": __("Erreur"),
|
||||
};
|
||||
|
||||
return labels[status] || status || __("À vérifier");
|
||||
}
|
||||
|
||||
|
||||
function get_review_color(status) {
|
||||
const colors = {
|
||||
"To Review": "orange",
|
||||
"Supplier Matched": "blue",
|
||||
"Ready for Import": "blue",
|
||||
"Imported": "green",
|
||||
"Ignored": "grey",
|
||||
"Error": "red",
|
||||
};
|
||||
|
||||
return colors[status] || "grey";
|
||||
}
|
||||
|
||||
|
||||
function render_supplier_summary(frm) {
|
||||
const field = frm.get_field(
|
||||
"supplier_summary_html"
|
||||
);
|
||||
|
||||
if (!field || !field.$wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const electronicAddress = [
|
||||
frm.doc.remote_supplier_electronic_scheme,
|
||||
frm.doc.remote_supplier_electronic_value,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ");
|
||||
|
||||
const locked = Boolean(
|
||||
frm.doc.source_document
|
||||
);
|
||||
|
||||
const lockedNotice = locked
|
||||
? `
|
||||
<div
|
||||
class="alert alert-info"
|
||||
style="margin: 14px 0 0;"
|
||||
>
|
||||
${__(
|
||||
"Le rapprochement est verrouillé car une facture d’achat ERPNext a déjà été créée."
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
field.$wrapper.html(`
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
margin: 4px 0 18px;
|
||||
"
|
||||
>
|
||||
<div class="frappe-card" style="padding: 18px;">
|
||||
<div class="text-muted small">
|
||||
${__("Émetteur de la facture")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 6px 0 14px;
|
||||
"
|
||||
>
|
||||
${display_value(
|
||||
frm.doc.remote_supplier_name
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 7px;">
|
||||
<strong>${__("N° TVA reçu")} :</strong>
|
||||
${display_value(
|
||||
frm.doc.remote_supplier_vat_id
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>${__("Adresse électronique")} :</strong>
|
||||
${display_value(electronicAddress)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="frappe-card" style="padding: 18px;">
|
||||
<div class="text-muted small">
|
||||
${__("Correspondance ERPNext")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 6px 0 14px;
|
||||
"
|
||||
>
|
||||
${supplier_link(frm.doc.supplier)}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 7px;">
|
||||
<strong>${__("Méthode")} :</strong>
|
||||
${escape_value(
|
||||
get_match_label(
|
||||
frm.doc.supplier_match_method
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span
|
||||
class="indicator-pill ${get_review_color(
|
||||
frm.doc.review_status
|
||||
)}"
|
||||
>
|
||||
${escape_value(
|
||||
get_review_label(
|
||||
frm.doc.review_status
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${lockedNotice}
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
function add_supplier_actions(frm) {
|
||||
if (frm.doc.source_document) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = __("Fournisseur");
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Relancer le rapprochement"),
|
||||
() => refresh_supplier_match(frm),
|
||||
group
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
frm.doc.supplier
|
||||
? __("Changer de fournisseur")
|
||||
: __("Associer un fournisseur existant"),
|
||||
() => select_supplier(frm),
|
||||
group
|
||||
);
|
||||
|
||||
if (frm.doc.supplier) {
|
||||
frm.add_custom_button(
|
||||
__("Supprimer l’association"),
|
||||
() => clear_supplier_match(frm),
|
||||
group
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function refresh_supplier_match(frm) {
|
||||
const response = await frappe.call({
|
||||
method:
|
||||
"enuxia_einvoice.inbound_import."
|
||||
+ "refresh_supplier_match",
|
||||
args: {
|
||||
exchange_name: frm.doc.name,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __(
|
||||
"Recherche du fournisseur…"
|
||||
),
|
||||
});
|
||||
|
||||
const result = response.message || {};
|
||||
|
||||
await frm.reload_doc();
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Rapprochement fournisseur"),
|
||||
indicator: result.matched
|
||||
? "green"
|
||||
: "orange",
|
||||
message:
|
||||
result.message
|
||||
|| __("Traitement terminé."),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function select_supplier(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: frm.doc.supplier
|
||||
? __("Changer de fournisseur")
|
||||
: __("Associer un fournisseur"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Fournisseur ERPNext"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
reqd: 1,
|
||||
default: frm.doc.supplier || "",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Associer"),
|
||||
async primary_action(values) {
|
||||
dialog.hide();
|
||||
|
||||
const response = await frappe.call({
|
||||
method:
|
||||
"enuxia_einvoice.inbound_import."
|
||||
+ "assign_supplier",
|
||||
args: {
|
||||
exchange_name: frm.doc.name,
|
||||
supplier: values.supplier,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __(
|
||||
"Association du fournisseur…"
|
||||
),
|
||||
});
|
||||
|
||||
const result =
|
||||
response.message || {};
|
||||
|
||||
await frm.reload_doc();
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Fournisseur associé"),
|
||||
indicator: result.vat_matches
|
||||
? "green"
|
||||
: "orange",
|
||||
message: result.vat_matches
|
||||
? __(
|
||||
"Le fournisseur a été associé et son numéro de TVA correspond à celui reçu."
|
||||
)
|
||||
: __(
|
||||
"Le fournisseur a été associé, mais son numéro de TVA ne correspond pas exactement à celui reçu."
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
function clear_supplier_match(frm) {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Supprimer l’association avec le fournisseur ERPNext ? Le fournisseur lui-même ne sera pas supprimé."
|
||||
),
|
||||
async () => {
|
||||
const response = await frappe.call({
|
||||
method:
|
||||
"enuxia_einvoice.inbound_import."
|
||||
+ "clear_supplier_match",
|
||||
args: {
|
||||
exchange_name: frm.doc.name,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __(
|
||||
"Suppression de l’association…"
|
||||
),
|
||||
});
|
||||
|
||||
const result =
|
||||
response.message || {};
|
||||
|
||||
await frm.reload_doc();
|
||||
|
||||
frappe.show_alert({
|
||||
message:
|
||||
result.message
|
||||
|| __("Association supprimée."),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
refresh(frm) {
|
||||
add_status_indicator(frm);
|
||||
render_einvoice_summary(frm);
|
||||
|
||||
if (frm.doc.docstatus !== 1) {
|
||||
return;
|
||||
@@ -14,30 +14,140 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
});
|
||||
|
||||
|
||||
function add_status_indicator(frm) {
|
||||
const status =
|
||||
frm.doc.einvoice_status || "Not Checked";
|
||||
function get_payment_status(frm) {
|
||||
const storedStatus =
|
||||
frm.doc.einvoice_payment_status
|
||||
|| "Not Applicable";
|
||||
|
||||
if (
|
||||
storedStatus === "Reported"
|
||||
|| storedStatus === "Error"
|
||||
) {
|
||||
return storedStatus;
|
||||
}
|
||||
|
||||
if (!frm.doc.einvoice_remote_id) {
|
||||
return "Not Applicable";
|
||||
}
|
||||
|
||||
const outstandingAmount = Number(
|
||||
frm.doc.outstanding_amount || 0
|
||||
);
|
||||
|
||||
if (Math.abs(outstandingAmount) < 0.000001) {
|
||||
return "To Report";
|
||||
}
|
||||
|
||||
return "Waiting for Payment";
|
||||
}
|
||||
|
||||
|
||||
function render_einvoice_summary(frm) {
|
||||
const field = frm.get_field(
|
||||
"einvoice_summary_html"
|
||||
);
|
||||
|
||||
if (!field || !field.$wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transportStatus =
|
||||
frm.doc.einvoice_status
|
||||
|| "Not Checked";
|
||||
|
||||
const paymentStatus =
|
||||
get_payment_status(frm);
|
||||
|
||||
const transportLabels = {
|
||||
"Not Checked": __("Non vérifiée"),
|
||||
"Blocked": __("Bloquée"),
|
||||
"Ready": __("Prête"),
|
||||
"Queued": __("En attente"),
|
||||
"Sent": __("Envoyée"),
|
||||
"Delivered": __("Livrée"),
|
||||
"Accepted": __("Acceptée"),
|
||||
"Rejected": __("Rejetée"),
|
||||
"Error": __("Erreur"),
|
||||
"Cancelled": __("Annulée"),
|
||||
};
|
||||
|
||||
const paymentLabels = {
|
||||
"Not Applicable": __("Non applicable"),
|
||||
"Waiting for Payment": __(
|
||||
"En attente de paiement"
|
||||
),
|
||||
"To Report": __("À déclarer"),
|
||||
"Reported": __("Encaissée"),
|
||||
"Error": __("Erreur"),
|
||||
};
|
||||
|
||||
const colors = {
|
||||
"Not Checked": "grey",
|
||||
"Not Applicable": "grey",
|
||||
"Blocked": "red",
|
||||
"Ready": "blue",
|
||||
"Queued": "orange",
|
||||
"Sent": "blue",
|
||||
"Delivered": "green",
|
||||
"Accepted": "green",
|
||||
"Rejected": "red",
|
||||
"Error": "red",
|
||||
"Ready": "blue",
|
||||
"Sent": "blue",
|
||||
"To Report": "blue",
|
||||
"Queued": "orange",
|
||||
"Waiting for Payment": "orange",
|
||||
"Delivered": "green",
|
||||
"Accepted": "green",
|
||||
"Reported": "green",
|
||||
"Cancelled": "grey",
|
||||
};
|
||||
|
||||
frm.dashboard.add_indicator(
|
||||
__(
|
||||
"Facturation électronique : {0}",
|
||||
[status]
|
||||
),
|
||||
colors[status] || "grey"
|
||||
);
|
||||
const renderCard = (
|
||||
title,
|
||||
label,
|
||||
color
|
||||
) => `
|
||||
<div
|
||||
class="frappe-card"
|
||||
style="
|
||||
padding: 16px;
|
||||
min-height: 82px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="text-muted small"
|
||||
style="margin-bottom: 10px;"
|
||||
>
|
||||
${frappe.utils.escape_html(title)}
|
||||
</div>
|
||||
|
||||
<span class="indicator-pill ${color}">
|
||||
${frappe.utils.escape_html(label)}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
field.$wrapper.html(`
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
margin: 4px 0 16px;
|
||||
"
|
||||
>
|
||||
${renderCard(
|
||||
__("Transmission électronique"),
|
||||
transportLabels[transportStatus]
|
||||
|| transportStatus,
|
||||
colors[transportStatus] || "grey"
|
||||
)}
|
||||
|
||||
${renderCard(
|
||||
__("Encaissement électronique"),
|
||||
paymentLabels[paymentStatus]
|
||||
|| paymentStatus,
|
||||
colors[paymentStatus] || "grey"
|
||||
)}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,18 +182,27 @@ function add_einvoice_buttons(frm) {
|
||||
group
|
||||
);
|
||||
|
||||
const paymentStatus =
|
||||
get_payment_status(frm);
|
||||
|
||||
if (
|
||||
Number(frm.doc.outstanding_amount || 0)
|
||||
=== 0
|
||||
paymentStatus === "To Report"
|
||||
|| paymentStatus === "Error"
|
||||
) {
|
||||
const paymentButtonLabel = (
|
||||
paymentStatus === "Error"
|
||||
? __("Réessayer la déclaration")
|
||||
: __("Déclarer l’encaissement")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Déclarer l’encaissement"),
|
||||
paymentButtonLabel,
|
||||
() => confirm_payment_received(frm),
|
||||
group
|
||||
);
|
||||
|
||||
frm.change_custom_button_type(
|
||||
__("Déclarer l’encaissement"),
|
||||
paymentButtonLabel,
|
||||
group,
|
||||
"primary"
|
||||
);
|
||||
@@ -324,6 +443,25 @@ function show_result(result, title) {
|
||||
);
|
||||
}
|
||||
|
||||
if (result.payment_status) {
|
||||
const labels = {
|
||||
"To Report": __("À déclarer"),
|
||||
"Reported": __("Encaissée"),
|
||||
"Error": __("Erreur"),
|
||||
};
|
||||
|
||||
parts.push(
|
||||
"<p><strong>"
|
||||
+ __("Statut d’encaissement")
|
||||
+ " :</strong> "
|
||||
+ frappe.utils.escape_html(
|
||||
labels[result.payment_status]
|
||||
|| result.payment_status
|
||||
)
|
||||
+ "</p>"
|
||||
);
|
||||
}
|
||||
|
||||
if (result.provider_status) {
|
||||
parts.push(
|
||||
"<p><strong>"
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2026, Enuxia and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_einvoice_payment_status(doc) {
|
||||
const storedStatus =
|
||||
doc.einvoice_payment_status
|
||||
|| "Not Applicable";
|
||||
|
||||
if (
|
||||
storedStatus === "Reported"
|
||||
|| storedStatus === "Error"
|
||||
) {
|
||||
return storedStatus;
|
||||
}
|
||||
|
||||
if (!doc.einvoice_remote_id) {
|
||||
return "Not Applicable";
|
||||
}
|
||||
|
||||
const outstandingAmount = Number(
|
||||
doc.outstanding_amount || 0
|
||||
);
|
||||
|
||||
if (Math.abs(outstandingAmount) < 0.000001) {
|
||||
return "To Report";
|
||||
}
|
||||
|
||||
return "Waiting for Payment";
|
||||
}
|
||||
|
||||
|
||||
function render_status_badge(
|
||||
status,
|
||||
labels,
|
||||
colors
|
||||
) {
|
||||
const label = labels[status] || status || "-";
|
||||
const color = colors[status] || "grey";
|
||||
|
||||
return `
|
||||
<span class="indicator-pill ${color}">
|
||||
${frappe.utils.escape_html(label)}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
async function apply_einvoice_filters(
|
||||
listview,
|
||||
filters
|
||||
) {
|
||||
await listview.filter_area.clear(false);
|
||||
await listview.filter_area.set(filters);
|
||||
await listview.refresh();
|
||||
}
|
||||
|
||||
|
||||
frappe.listview_settings["Sales Invoice"] = {
|
||||
add_fields: [
|
||||
"einvoice_status",
|
||||
"einvoice_payment_status",
|
||||
"einvoice_remote_id",
|
||||
"outstanding_amount",
|
||||
"docstatus",
|
||||
],
|
||||
|
||||
onload(listview) {
|
||||
const group = __("Facturation électronique");
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("Toutes les factures"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[]
|
||||
),
|
||||
group
|
||||
);
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("Prêtes à envoyer"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[
|
||||
[
|
||||
"Sales Invoice",
|
||||
"docstatus",
|
||||
"=",
|
||||
1,
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_status",
|
||||
"=",
|
||||
"Ready",
|
||||
],
|
||||
]
|
||||
),
|
||||
group
|
||||
);
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("En attente de paiement"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[
|
||||
[
|
||||
"Sales Invoice",
|
||||
"docstatus",
|
||||
"=",
|
||||
1,
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_remote_id",
|
||||
"is",
|
||||
"set",
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"outstanding_amount",
|
||||
">",
|
||||
0,
|
||||
],
|
||||
]
|
||||
),
|
||||
group
|
||||
);
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("Encaissements à déclarer"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[
|
||||
[
|
||||
"Sales Invoice",
|
||||
"docstatus",
|
||||
"=",
|
||||
1,
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_remote_id",
|
||||
"is",
|
||||
"set",
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"outstanding_amount",
|
||||
"=",
|
||||
0,
|
||||
],
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_payment_status",
|
||||
"!=",
|
||||
"Reported",
|
||||
],
|
||||
]
|
||||
),
|
||||
group
|
||||
);
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("Encaissements déclarés"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_payment_status",
|
||||
"=",
|
||||
"Reported",
|
||||
],
|
||||
]
|
||||
),
|
||||
group
|
||||
);
|
||||
|
||||
listview.page.add_inner_button(
|
||||
__("Erreurs d’encaissement"),
|
||||
() => apply_einvoice_filters(
|
||||
listview,
|
||||
[
|
||||
[
|
||||
"Sales Invoice",
|
||||
"einvoice_payment_status",
|
||||
"=",
|
||||
"Error",
|
||||
],
|
||||
]
|
||||
),
|
||||
group
|
||||
);
|
||||
},
|
||||
|
||||
formatters: {
|
||||
einvoice_status(value) {
|
||||
const labels = {
|
||||
"Not Checked": __("Non vérifiée"),
|
||||
"Blocked": __("Bloquée"),
|
||||
"Ready": __("Prête"),
|
||||
"Queued": __("En attente"),
|
||||
"Sent": __("Envoyée"),
|
||||
"Delivered": __("Livrée"),
|
||||
"Accepted": __("Acceptée"),
|
||||
"Rejected": __("Rejetée"),
|
||||
"Error": __("Erreur"),
|
||||
"Cancelled": __("Annulée"),
|
||||
};
|
||||
|
||||
const colors = {
|
||||
"Not Checked": "grey",
|
||||
"Blocked": "red",
|
||||
"Ready": "blue",
|
||||
"Queued": "orange",
|
||||
"Sent": "blue",
|
||||
"Delivered": "green",
|
||||
"Accepted": "green",
|
||||
"Rejected": "red",
|
||||
"Error": "red",
|
||||
"Cancelled": "grey",
|
||||
};
|
||||
|
||||
return render_status_badge(
|
||||
value || "Not Checked",
|
||||
labels,
|
||||
colors
|
||||
);
|
||||
},
|
||||
|
||||
einvoice_payment_status(
|
||||
value,
|
||||
df,
|
||||
doc
|
||||
) {
|
||||
const status =
|
||||
get_einvoice_payment_status(doc);
|
||||
|
||||
const labels = {
|
||||
"Not Applicable": __("Non applicable"),
|
||||
"Waiting for Payment": __(
|
||||
"En attente de paiement"
|
||||
),
|
||||
"To Report": __("À déclarer"),
|
||||
"Reported": __("Encaissée"),
|
||||
"Error": __("Erreur"),
|
||||
};
|
||||
|
||||
const colors = {
|
||||
"Not Applicable": "grey",
|
||||
"Waiting for Payment": "orange",
|
||||
"To Report": "blue",
|
||||
"Reported": "green",
|
||||
"Error": "red",
|
||||
};
|
||||
|
||||
return render_status_badge(
|
||||
status,
|
||||
labels,
|
||||
colors
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -14,7 +14,7 @@ CUSTOM_FIELDS = {
|
||||
"fieldname": "einvoice_section",
|
||||
"label": "Facturation électronique",
|
||||
"fieldtype": "Section Break",
|
||||
"insert_after": "tax_id",
|
||||
"insert_after": "territory",
|
||||
"collapsible": 1,
|
||||
},
|
||||
{
|
||||
@@ -152,11 +152,22 @@ CUSTOM_FIELDS = {
|
||||
"fieldtype": "Tab Break",
|
||||
"insert_after": "loyalty_amount",
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_summary_section",
|
||||
"label": "Résumé",
|
||||
"fieldtype": "Section Break",
|
||||
"insert_after": "einvoice_tab",
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_summary_html",
|
||||
"fieldtype": "HTML",
|
||||
"insert_after": "einvoice_summary_section",
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_invoice_section",
|
||||
"label": "Configuration de la facture",
|
||||
"fieldtype": "Section Break",
|
||||
"insert_after": "einvoice_tab",
|
||||
"insert_after": "einvoice_summary_html",
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_operation_category",
|
||||
@@ -199,6 +210,7 @@ CUSTOM_FIELDS = {
|
||||
"default": "Not Checked",
|
||||
"read_only": 1,
|
||||
"in_standard_filter": 1,
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_external_id",
|
||||
@@ -261,6 +273,7 @@ CUSTOM_FIELDS = {
|
||||
"read_only": 1,
|
||||
"allow_on_submit": 1,
|
||||
"in_standard_filter": 1,
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "einvoice_payment_reported_at",
|
||||
|
||||
@@ -109,6 +109,27 @@ class TestInboundInvoiceImport(unittest.TestCase):
|
||||
"Delivered",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
values["remote_supplier_name"],
|
||||
"Tricatel",
|
||||
)
|
||||
self.assertEqual(
|
||||
values["remote_supplier_vat_id"],
|
||||
"FR15000000001",
|
||||
)
|
||||
self.assertEqual(
|
||||
values[
|
||||
"remote_supplier_electronic_scheme"
|
||||
],
|
||||
"0225",
|
||||
)
|
||||
self.assertEqual(
|
||||
values[
|
||||
"remote_supplier_electronic_value"
|
||||
],
|
||||
"315143296_4919",
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_tax_identifier(self) -> None:
|
||||
self.assertEqual(
|
||||
|
||||
Reference in New Issue
Block a user