Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
63 changes: 63 additions & 0 deletions erpnext_france/erpnext_france/doctype/sepa_mandate/sepa_mandate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2026, Scopen and contributors
// For license information, please see license.txt

frappe.ui.form.on('SEPA Mandate', {
refresh: function(frm) {
// Add button to update to recurring
if (frm.doc.sequence_type === 'FRST' && frm.doc.status === 'Active') {
frm.add_custom_button(__('Update to Recurring'), function() {
frappe.call({
method: 'update_to_recurring',
doc: frm.doc,
callback: function(r) {
frm.reload_doc();
}
});
});
}

// Set color indicator based on status
if (frm.doc.status) {
var color_map = {
'Draft': 'gray',
'Active': 'green',
'Cancelled': 'red'
};
frm.page.set_indicator(frm.doc.status, color_map[frm.doc.status]);
}
},

customer: function(frm) {
// Set query for bank account to only show customer's bank accounts
if (frm.doc.customer) {
frm.set_query('bank_account', function() {
return {
filters: {
'party_type': 'Customer',
'party': frm.doc.customer
}
};
});
}
},

onload: function(frm) {
// Set query for bank account
if (frm.doc.customer) {
frm.trigger('customer');
}
},

bank_account: function(frm) {
// Fetch IBAN and BIC when bank account is selected
if (frm.doc.bank_account) {
frappe.db.get_value('Bank Account', frm.doc.bank_account, ['iban', 'swift_number'], function(r) {
if (r) {
if (!r.iban || !r.swift_number) {
frappe.msgprint(__('Please ensure IBAN and BIC are filled in the bank account'));
}
}
});
}
}
});
136 changes: 136 additions & 0 deletions erpnext_france/erpnext_france/doctype/sepa_mandate/sepa_mandate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
"actions": [],
"autoname": "format:MANDATE-{####}",
"creation": "2026-01-15 00:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"bank_account",
"column_break_1",
"rum",
"signature_date",
"section_break_2",
"mandate_type",
"sequence_type",
"column_break_3",
"status"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "bank_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Bank Account",
"options": "Bank Account",
"reqd": 1
},
{
"fieldname": "rum",
"fieldtype": "Data",
"in_list_view": 1,
"label": "RUM (Référence Unique de Mandat)",
"reqd": 1,
"unique": 1
},
{
"fieldname": "signature_date",
"fieldtype": "Date",
"label": "Signature Date",
"reqd": 1
},
{
"fieldname": "mandate_type",
"fieldtype": "Select",
"label": "Mandate Type",
"options": "CORE\nB2B",
"reqd": 1,
"default": "CORE"
},
{
"fieldname": "sequence_type",
"fieldtype": "Select",
"label": "Sequence Type",
"options": "FRST\nRCUR",
"reqd": 1,
"default": "FRST"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Draft\nActive\nCancelled",
"reqd": 1,
"default": "Draft"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"links": [],
"modified": "2026-01-15 00:00:00.000000",
"modified_by": "Administrator",
"module": "ERPNext France",
"name": "SEPA Mandate",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
56 changes: 56 additions & 0 deletions erpnext_france/erpnext_france/doctype/sepa_mandate/sepa_mandate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) 2025, Scopen and Contributors
# See license.txt

import frappe
from frappe import _
from frappe.model.document import Document


class SEPAMandate(Document):
def validate(self):
self.validate_unique_active_mandate()
self.validate_bank_account()

def validate_unique_active_mandate(self):
"""Ensure only one active mandate per customer and bank account (IBAN)"""
if self.status == "Active":
# Get the IBAN from the linked bank account
bank_account = frappe.get_doc("Bank Account", self.bank_account)

existing = frappe.db.exists(
"SEPA Mandate",
{
"customer": self.customer,
"bank_account": self.bank_account,
"status": "Active",
"name": ["!=", self.name]
}
)

if existing:
frappe.throw(
_("An active SEPA Mandate already exists for this customer and bank account. Please cancel the existing mandate before activating a new one.")
)

def validate_bank_account(self):
"""Validate that the bank account belongs to the customer"""
if self.bank_account and self.customer:
bank_account = frappe.get_doc("Bank Account", self.bank_account)

# Check if the bank account is linked to the customer
if bank_account.party_type != "Customer" or bank_account.party != self.customer:
frappe.throw(
_("The selected bank account does not belong to the customer {0}").format(self.customer)
)

def on_update(self):
"""Update sequence type after first successful debit"""
pass

@frappe.whitelist()
def update_to_recurring(self):
"""Update sequence type from FRST to RCUR after first successful debit"""
if self.sequence_type == "FRST" and self.status == "Active":
self.sequence_type = "RCUR"
self.save()
frappe.msgprint(_("Mandate updated to recurring (RCUR) after first successful debit"))
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2026, Scopen and contributors
// For license information, please see license.txt

frappe.ui.form.on('SEPA Payment Bordereau', {
refresh: function(frm) {
// Add Validate button
if (frm.doc.status === 'Draft' && frm.doc.lines && frm.doc.lines.length > 0) {
frm.add_custom_button(__('Validate Bordereau'), function() {
frappe.call({
method: 'validate_bordereau',
doc: frm.doc,
callback: function(r) {
frm.reload_doc();
}
});
}).addClass('btn-primary');
}

// Add Generate SEPA File button
if (frm.doc.status === 'Validated' || frm.doc.status === 'Exported') {
frm.add_custom_button(__('Generate SEPA File'), function() {
frappe.call({
method: 'generate_sepa_file',
doc: frm.doc,
callback: function(r) {
if (r.message) {
frappe.msgprint(__('SEPA file generated: {0}', [r.message]));
frm.reload_doc();
}
}
});
}).addClass('btn-primary');
}

// Add Mark as Sent button
if (frm.doc.status === 'Exported') {
frm.add_custom_button(__('Mark as Sent'), function() {
frappe.call({
method: 'mark_as_sent',
doc: frm.doc,
callback: function(r) {
frm.reload_doc();
}
});
});
}

// Set color indicator based on status
if (frm.doc.status) {
var color_map = {
'Draft': 'gray',
'Validated': 'blue',
'Exported': 'orange',
'Sent': 'purple',
'Partial Rejections': 'red',
'Closed': 'green'
};
frm.page.set_indicator(frm.doc.status, color_map[frm.doc.status]);
}
},

payment_type: function(frm) {
// Update naming when payment type changes
frm.trigger('set_naming');
},

onload: function(frm) {
// Set query for bank account to only show company's bank accounts
if (frm.doc.company) {
frm.set_query('bank_account', function() {
return {
filters: {
'company': frm.doc.company,
'is_company_account': 1
}
};
});
}
},

company: function(frm) {
// Set default bank account when company changes
if (frm.doc.company && !frm.doc.bank_account) {
frappe.db.get_value('Bank Account',
{'company': frm.doc.company, 'is_default': 1},
'name',
function(r) {
if (r && r.name) {
frm.set_value('bank_account', r.name);
}
}
);
}
}
});

frappe.ui.form.on('SEPA Payment Bordereau Line', {
amount: function(frm) {
// Recalculate total when line amount changes
frm.trigger('calculate_total');
},

lines_remove: function(frm) {
// Recalculate total when line is removed
frm.trigger('calculate_total');
}
});
Loading