You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
To ensure ease of use and prevent errors, I have created the following rule, which has greatly contributed to reducing technical support requests. I suggest that it be applied to the current application.
It is preferable to apply 1-6 "optional"
ZATCA rules from number 7-9 "mandatory"
// // 1. validate the invoice serial if Credit note
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
let serial = frm.doc.naming_series; // The sales invoice's serial
let isReturn = frm.doc.is_return;
let isDebitNote = frm.doc.is_debit_note;
// Check for Credit Note / Return
if (isReturn) {
if (!/CN|RET/i.test(serial)) {
frappe.throw(__('The serial must contain "CN" or "RET" for a Credit Note/Return. Please update the serial.'));
}
}
// Check for Debit Note
if (isDebitNote) {
if (!/DN/i.test(serial)) {
frappe.throw(__('The serial must contain "DN" for a Debit Note. Please update the serial.'));
}
}
}
});
// //2. update the series year automatically depending on the posting date year
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
// Only run if the document is new and not in draft state
if (frm.is_new && frm.doc.docstatus === 0) {
// Get the year from the posting date
const postingYear = new Date(frm.doc.posting_date).getFullYear();
// Check if the naming series contains ".YYYY."
if (frm.doc.naming_series && frm.doc.naming_series.includes('.YYYY.')) {
const updatedSeries = frm.doc.naming_series.replace('.YYYY.', `.${postingYear}.`);
frm.set_value('naming_series', updatedSeries);
}
}
}
});
// // 3. script for making sure that no previous draft invoice before submitting a new sales Invoice
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
return new Promise((resolve, reject) => {
// Check if the 'abbr' field exists in the Sales Invoice doctype
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
// Build the filters dynamically
let filters = [
['docstatus', '=', 0], // Only draft invoices
['creation', '<', frm.doc.creation], // Created before the current invoice
['naming_series', '=', frm.doc.naming_series] // Same naming series
];
if (abbr_field_exists && frm.doc.abbr) {
filters.push(['abbr', '=', frm.doc.abbr]); // Add abbreviation filter if the field exists
}
// Fetch the list of prior draft Sales Invoices
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Sales Invoice',
filters: filters,
fields: ['name'],
order_by: 'creation desc',
limit: 1 // Check if any prior draft exists
},
callback: function(response) {
let previous_draft = response.message && response.message[0];
if (previous_draft) {
frappe.msgprint(__('Please submit the previous Sales Invoice ({0}) before submitting this one.', [previous_draft.name]));
reject(); // Prevent the submission
} else {
resolve(); // Allow the submission
}
}
});
});
});
}
});
// 4. script for making sure that the date of the new sales Invoice is after the date of the last sales invoice
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
return new Promise((resolve, reject) => {
// Fetch doctype metadata to check if the 'abbr' field exists
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
// Build filters dynamically based on the existence of the 'abbr' field
let filters = [
['naming_series', '=', frm.doc.naming_series], // Same naming series
['docstatus', '=', 1], // Only submitted invoices
];
if (abbr_field_exists && frm.doc.abbr) {
filters.push(['abbr', '=', frm.doc.abbr]); // Add abbreviation filter if the field exists
}
// Fetch the last submitted Sales Invoice based on the filters
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Sales Invoice',
filters: filters,
fields: ['posting_date'],
order_by: 'posting_date desc',
limit: 1 // Get the last submitted invoice
},
callback: function(response) {
let last_submitted = response.message && response.message[0];
if (last_submitted && frm.doc.posting_date < last_submitted.posting_date) {
frappe.msgprint(__('The posting date cannot be before the last invoice date: {0}', [last_submitted.posting_date]));
reject(); // Prevent saving
} else {
resolve(); // Allow saving
}
}
});
});
});
}
});
// 5. function to check that The total of the credit note cannot exceed the total of the original sales invoice
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
// Check if this is a Credit Note (negative invoice)
if (frm.doc.is_return && frm.doc.return_against) {
return new Promise((resolve, reject) => {
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Sales Invoice',
name: frm.doc.return_against // The original invoice linked to this credit note
},
callback: function(response) {
let original_invoice = response.message;
if (original_invoice && Math.abs(frm.doc.grand_total) > original_invoice.grand_total) {
frappe.msgprint(__('The total of the credit note cannot exceed the total of the original sales invoice ({0}).', [original_invoice.grand_total]));
reject(); // Prevent submission
} else {
resolve(); // Allow submission
}
}
});
});
}
}
});
// 6. function to update the tax template automatically when changing the company in SI
frappe.ui.form.on('Sales Invoice', {
company: function(frm) {
if (frm.doc.company) {
// Fetch the default tax template for the selected company
frappe.db.get_list('Sales Taxes and Charges Template', {
filters: {
company: frm.doc.company,
is_default: 1 // Look for the default tax template
},
fields: ['name'],
limit: 1
}).then((templates) => {
if (templates.length > 0) {
// Set the default tax template
frm.set_value('taxes_and_charges', templates[0].name);
frm.trigger('taxes_and_charges'); // Reapply template to update tax details
} else {
// Clear the field if no default template is found
frm.set_value('taxes_and_charges', null);
}
});
}
}
});
// 7. ZATCA check the submission of the previous invoice to Zatca before submitting the new SI
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
return new Promise((resolve, reject) => {
// Fetch doctype metadata to check if the 'abbr' field exists
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
// Build filters dynamically
let filters = [
['docstatus', '=', 1], // Only submitted invoices
['creation', '<', frm.doc.creation], // Created before the current invoice
['naming_series', '=', frm.doc.naming_series] // Same naming series
];
if (abbr_field_exists && frm.doc.abbr) {
filters.push(['abbr', '=', frm.doc.abbr]); // Add abbreviation filter if the field exists
}
// Fetch the most recent previous Sales Invoice
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Sales Invoice',
filters: filters,
fields: ['name', 'custom_uuid'],
order_by: 'creation desc',
limit: 1 // Get the most recent previous invoice
},
callback: function(response) {
let previous_invoice = response.message && response.message[0];
if (previous_invoice && frm.doc.company.custom_zatca_invoice_enabled === 1) {
if (previous_invoice.custom_uuid && previous_invoice.custom_uuid.length >= 36) {
resolve(); // custom_uuid is valid, allow the submission
} else {
frappe.msgprint(__('The previous Sales Invoice ({0}) is not submitted to Zatca. Please submit the previous Invoice before submitting this invoice.', [previous_invoice.name]));
reject(); // Prevent the submission
}
} else {
// No previous invoice found, allow submission
resolve();
}
}
});
});
});
}
});
// 8. ZATCA function to set Customer address to non mandatory for B2C,
frappe.ui.form.on('Sales Invoice', {
validate: async function (frm) {
if (frm.doc.customer) {
try {
let { message: customer } = await frappe.db.get_value('Customer', frm.doc.customer, ['custom_b2c', 'territory']);
if (customer) {
// If the territory is Saudi Arabia, apply mandatory checks
if (customer.territory === 'Saudi Arabia') {
if (!customer.custom_b2c) {
if (!frm.doc.tax_id) {
frappe.throw(__('Tax ID is mandatory if "Custom B2C" is unchecked for the selected Customer and the territory is Saudi Arabia.'));
}
if (!frm.doc.customer_address) {
frappe.throw(__('Customer Address is mandatory if "Custom B2C" is unchecked for the selected Customer and the territory is Saudi Arabia.'));
}
if (frm.doc.tax_id && frm.doc.tax_id.length !== 15) {
frappe.throw(__('Tax ID must be 15 digits.'));
}
}
}
}
} catch (error) {
frappe.validated = false; // Ensure saving is blocked if an error occurs
throw error;
}
}
},
customer: function (frm) {
if (frm.doc.customer) {
frappe.db.get_value('Customer', frm.doc.customer, ['custom_b2c', 'territory'], (r) => {
if (r) {
const is_mandatory = r.territory === 'Saudi Arabia' && !r.custom_b2c ? 1 : 0; // Mandatory if 'custom_b2c' is unchecked and territory is Saudi Arabia
frm.set_df_property('tax_id', 'reqd', is_mandatory);
frm.set_df_property('customer_address', 'reqd', is_mandatory);
}
});
}
}
});
// 9. Zatca script Check if posting date of the sales invoice is within one day of the current date
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
return new Promise((resolve, reject) => {
// Check if posting date is within one day of the current date
if (!is_within_one_day_of_current_date(frm.doc.posting_date)) {
frappe.confirm(
// Warning message
__('According to Saudi Tax Authority, the sales invoice should be submitted on the same day. This actions can cause you penalties from Zatca. Are you sure you want to continue?'),
// Yes button callback
function () {
resolve(); // Allow saving
},
// No button callback
function () {
reject(); // Prevent saving
}
);
return;
}
resolve(); // Allow saving if the date is valid
});
}
});
function is_within_one_day_of_current_date(posting_date) {
let today = frappe.datetime.get_today();
let one_day_before_today = frappe.datetime.add_days(today, -1);
let one_day_after_today = frappe.datetime.add_days(today, 1);
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
To ensure ease of use and prevent errors, I have created the following rule, which has greatly contributed to reducing technical support requests. I suggest that it be applied to the current application.
It is preferable to apply 1-6 "optional"
ZATCA rules from number 7-9 "mandatory"
// // 1. validate the invoice serial if Credit note
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
let serial = frm.doc.naming_series; // The sales invoice's serial
let isReturn = frm.doc.is_return;
let isDebitNote = frm.doc.is_debit_note;
});
// //2. update the series year automatically depending on the posting date year
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
// Only run if the document is new and not in draft state
if (frm.is_new && frm.doc.docstatus === 0) {
// Get the year from the posting date
const postingYear = new Date(frm.doc.posting_date).getFullYear();
});
// // 3. script for making sure that no previous draft invoice before submitting a new sales Invoice
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
return new Promise((resolve, reject) => {
// Check if the 'abbr' field exists in the Sales Invoice doctype
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
});
// 4. script for making sure that the date of the new sales Invoice is after the date of the last sales invoice
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
return new Promise((resolve, reject) => {
// Fetch doctype metadata to check if the 'abbr' field exists
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
});
// 5. function to check that The total of the credit note cannot exceed the total of the original sales invoice
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
// Check if this is a Credit Note (negative invoice)
if (frm.doc.is_return && frm.doc.return_against) {
return new Promise((resolve, reject) => {
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Sales Invoice',
name: frm.doc.return_against // The original invoice linked to this credit note
},
callback: function(response) {
let original_invoice = response.message;
if (original_invoice && Math.abs(frm.doc.grand_total) > original_invoice.grand_total) {
frappe.msgprint(__('The total of the credit note cannot exceed the total of the original sales invoice ({0}).', [original_invoice.grand_total]));
reject(); // Prevent submission
} else {
resolve(); // Allow submission
}
}
});
});
}
}
});
// 6. function to update the tax template automatically when changing the company in SI
frappe.ui.form.on('Sales Invoice', {
company: function(frm) {
if (frm.doc.company) {
// Fetch the default tax template for the selected company
frappe.db.get_list('Sales Taxes and Charges Template', {
filters: {
company: frm.doc.company,
is_default: 1 // Look for the default tax template
},
fields: ['name'],
limit: 1
}).then((templates) => {
if (templates.length > 0) {
// Set the default tax template
frm.set_value('taxes_and_charges', templates[0].name);
frm.trigger('taxes_and_charges'); // Reapply template to update tax details
} else {
// Clear the field if no default template is found
frm.set_value('taxes_and_charges', null);
}
});
}
}
});
// *******************************************************************************************************************************
// ZATCA Validations
// 7. ZATCA check the submission of the previous invoice to Zatca before submitting the new SI
frappe.ui.form.on('Sales Invoice', {
before_submit: function(frm) {
return new Promise((resolve, reject) => {
// Fetch doctype metadata to check if the 'abbr' field exists
frappe.model.with_doctype('Sales Invoice', () => {
let meta = frappe.get_meta('Sales Invoice');
let abbr_field_exists = meta.fields.some(field => field.fieldname === 'abbr');
});
// 8. ZATCA function to set Customer address to non mandatory for B2C,
frappe.ui.form.on('Sales Invoice', {
validate: async function (frm) {
if (frm.doc.customer) {
try {
let { message: customer } = await frappe.db.get_value('Customer', frm.doc.customer, ['custom_b2c', 'territory']);
});
// 9. Zatca script Check if posting date of the sales invoice is within one day of the current date
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
return new Promise((resolve, reject) => {
// Check if posting date is within one day of the current date
if (!is_within_one_day_of_current_date(frm.doc.posting_date)) {
frappe.confirm(
// Warning message
__('According to Saudi Tax Authority, the sales invoice should be submitted on the same day. This actions can cause you penalties from Zatca. Are you sure you want to continue?'),
});
function is_within_one_day_of_current_date(posting_date) {
let today = frappe.datetime.get_today();
let one_day_before_today = frappe.datetime.add_days(today, -1);
let one_day_after_today = frappe.datetime.add_days(today, 1);
}
Beta Was this translation helpful? Give feedback.
All reactions