import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { bexioAuth } from '../../index';
import { BexioClient } from '../common/client';
export const createSalesQuoteAction = createAction({
auth: bexioAuth,
name: 'create_sales_quote',
displayName: 'Create Sales Quote',
description: 'Create a new product-based sales quote',
props: {
document_nr: Property.ShortText({
displayName: 'Document Number',
description: 'Quote number (required if automatic numbering is disabled)',
required: false,
}),
title: Property.ShortText({
displayName: 'Title',
description: 'Quote title',
required: false,
}),
contact_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Contact',
description: 'The contact for this quote',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const contacts = await client.get<Array<{
id: number;
contact_type_id: number;
name_1: string;
name_2?: string | null;
nr?: string | null;
}>>('/2.0/contact');
return {
disabled: false,
options: contacts.map((contact) => {
const name = contact.name_2
? `${contact.name_2} ${contact.name_1}`
: contact.name_1;
const label = contact.nr ? `${name} (#${contact.nr})` : name;
return {
label,
value: contact.id,
};
}),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load contacts',
options: [],
};
}
},
}),
contact_sub_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Contact Sub',
description: 'Contact sub-address (optional)',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const contacts = await client.get<Array<{
id: number;
contact_type_id: number;
name_1: string;
name_2?: string | null;
nr?: string | null;
}>>('/2.0/contact');
return {
disabled: false,
options: contacts.map((contact) => {
const name = contact.name_2
? `${contact.name_2} ${contact.name_1}`
: contact.name_1;
const label = contact.nr ? `${name} (#${contact.nr})` : name;
return {
label,
value: contact.id,
};
}),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load contacts',
options: [],
};
}
},
}),
user_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'User',
description: 'User responsible for this quote',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const users = await client.get<Array<{
id: number;
firstname?: string | null;
lastname?: string | null;
email: string;
}>>('/3.0/users');
return {
disabled: false,
options: users.map((user) => {
const name = user.firstname && user.lastname
? `${user.firstname} ${user.lastname}`
: user.email;
return {
label: name,
value: user.id,
};
}),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load users',
options: [],
};
}
},
}),
pr_project_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Project',
description: 'Project associated with this quote',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const projects = await client.get<Array<{
id: number;
name: string;
nr?: string;
}>>('/2.0/pr_project');
return {
disabled: false,
options: projects.map((project) => ({
label: project.nr ? `${project.name} (#${project.nr})` : project.name,
value: project.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load projects',
options: [],
};
}
},
}),
language_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Language',
description: 'Language for the quote',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const languages = await client.get<Array<{ id: number; name: string }>>('/2.0/language');
return {
disabled: false,
options: languages.map((lang) => ({
label: lang.name,
value: lang.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load languages',
options: [],
};
}
},
}),
bank_account_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Bank Account',
description: 'Bank account for payment',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const bankAccounts = await client.get<Array<{
id: number;
name: string;
iban_nr?: string;
bank_account_nr?: string;
}>>('/3.0/banking/accounts');
return {
disabled: false,
options: bankAccounts.map((account) => {
const iban = account.iban_nr || account.bank_account_nr;
return {
label: iban ? `${account.name} (${iban})` : account.name,
value: account.id,
};
}),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load bank accounts',
options: [],
};
}
},
}),
currency_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Currency',
description: 'Currency for this quote',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const currencies = await client.get<Array<{ id: number; name: string }>>('/currencies');
return {
disabled: false,
options: currencies.map((currency) => ({
label: currency.name,
value: currency.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load currencies',
options: [],
};
}
},
}),
payment_type_id: Property.Dropdown({
auth: bexioAuth,
displayName: 'Payment Type',
description: 'Payment type for this quote',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const paymentTypes = await client.get<Array<{ id: number; name: string }>>('/2.0/payment_type');
return {
disabled: false,
options: paymentTypes.map((type) => ({
label: type.name,
value: type.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load payment types',
options: [],
};
}
},
}),
header: Property.LongText({
displayName: 'Header',
description: 'Header text for the quote',
required: false,
}),
footer: Property.LongText({
displayName: 'Footer',
description: 'Footer text for the quote',
required: false,
}),
mwst_type: Property.StaticDropdown({
displayName: 'Tax Type',
description: 'How taxes are handled',
required: true,
defaultValue: 0,
options: {
disabled: false,
options: [
{ label: 'Including taxes', value: 0 },
{ label: 'Excluding taxes', value: 1 },
{ label: 'Exempt from taxes', value: 2 },
],
},
}),
mwst_is_net: Property.Checkbox({
displayName: 'Tax is Net',
description: 'If taxes are included, set to true to add taxes to total, false to include in total',
required: false,
defaultValue: false,
}),
show_position_taxes: Property.Checkbox({
displayName: 'Show Position Taxes',
description: 'Show taxes for each position',
required: false,
defaultValue: false,
}),
is_valid_from: Property.ShortText({
displayName: 'Valid From',
description: 'Quote valid from date (YYYY-MM-DD)',
required: true,
}),
is_valid_until: Property.ShortText({
displayName: 'Valid Until',
description: 'Quote valid until date (YYYY-MM-DD)',
required: true,
}),
contact_address_manual: Property.LongText({
displayName: 'Manual Contact Address',
description: 'Override contact address (leave empty to use contact address)',
required: false,
}),
delivery_address_type: Property.StaticDropdown({
displayName: 'Delivery Address Type',
description: 'Type of delivery address',
required: false,
defaultValue: 0,
options: {
disabled: false,
options: [
{ label: 'Use Invoice Address', value: 0 },
{ label: 'Use Custom Address', value: 1 },
],
},
}),
delivery_address_manual_text: Property.LongText({
displayName: 'Manual Delivery Address',
description: 'Custom delivery address (only used if delivery address type is set to custom)',
required: false,
}),
api_reference: Property.ShortText({
displayName: 'API Reference',
description: 'Reference for API use (can only be edited via API)',
required: false,
}),
viewed_by_client_at: Property.DateTime({
displayName: 'Viewed By Client At',
description: 'Date when the quote was viewed by the client',
required: false,
}),
kb_terms_of_payment_template_id: Property.Number({
displayName: 'Terms of Payment Template ID',
description: 'Terms of payment template ID',
required: false,
}),
template_slug: Property.Dropdown({
auth: bexioAuth,
displayName: 'Document Template',
description: 'Document template for the quote',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Bexio account first',
options: [],
};
}
try {
const client = new BexioClient(auth);
const templates = await client.get<Array<{
template_slug: string;
name: string;
is_default: boolean;
default_for_document_types: string[];
}>>('/3.0/document_templates');
return {
disabled: false,
options: templates.map((template) => ({
label: template.is_default ? `${template.name} (Default)` : template.name,
value: template.template_slug,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load templates',
options: [],
};
}
},
}),
positionFields: Property.DynamicProperties({
auth: bexioAuth,
displayName: 'Quote Positions',
description: 'Configure quote line items',
required: true,
refreshers: ['auth'],
props: async ({ auth }) => {
let units: Array<{ id: number; name: string }> = [];
let accounts: Array<{ id: number; account_no: string; name: string }> = [];
let taxes: Array<{ id: number; name: string; percentage: string }> = [];
if (auth) {
try {
const client = new BexioClient(auth);
units = await client.get<Array<{ id: number; name: string }>>('/2.0/unit').catch(() => []);
accounts = await client.get<Array<{ id: number; account_no: string; name: string }>>('/accounts').catch(() => []);
const taxesResponse = await client.get<Array<{
id: number;
name: string;
value: number;
display_name?: string;
}>>('/3.0/taxes?types=sales_tax&scope=active').catch(() => []);
taxes = taxesResponse.map((tax) => ({
id: tax.id,
name: tax.display_name || tax.name,
percentage: tax.value.toString(),
}));
} catch (error) {
// Ignore error, use empty array as fallback
}
}
const unitOptions = units.map((unit) => ({ label: unit.name, value: unit.id }));
const accountOptions = accounts.map((acc) => ({ label: `${acc.account_no} - ${acc.name}`, value: acc.id }));
const taxOptions = taxes.map((tax) => ({ label: `${tax.name} (${tax.percentage}%)`, value: tax.id }));
return {
positions: Property.Array({
displayName: 'Positions',
description: 'Quote line items',
required: true,
properties: {
type: Property.StaticDropdown({
displayName: 'Position Type',
description: 'Type of quote position',
required: true,
defaultValue: 'KbPositionCustom',
options: {
disabled: false,
options: [
{ label: 'Custom Position', value: 'KbPositionCustom' },
{ label: 'Article Position', value: 'KbPositionArticle' },
{ label: 'Text Position', value: 'KbPositionText' },
{ label: 'Subtotal Position', value: 'KbPositionSubtotal' },
{ label: 'Page Break Position', value: 'KbPositionPagebreak' },
{ label: 'Discount Position', value: 'KbPositionDiscount' },
],
},
}),
amount: Property.ShortText({
displayName: 'Amount',
description: 'Quantity/amount',
required: true,
}),
unit_id: unitOptions.length > 0
? Property.StaticDropdown({
displayName: 'Unit',
description: 'Unit of measurement',
required: false,
options: {
disabled: false,
options: unitOptions,
},
})
: Property.Number({
displayName: 'Unit ID',
description: 'Unit ID (use List Units action to find IDs)',
required: false,
}),
account_id: accountOptions.length > 0
? Property.StaticDropdown({
displayName: 'Account',
description: 'Account for this position',
required: false,
options: {
disabled: false,
options: accountOptions,
},
})
: Property.Number({
displayName: 'Account ID',
description: 'Account ID (use List Accounts action to find IDs)',
required: false,
}),
tax_id: taxOptions.length > 0
? Property.StaticDropdown({
displayName: 'Tax',
description: 'Tax rate (only active sales taxes)',
required: false,
options: {
disabled: false,
options: taxOptions,
},
})
: Property.Number({
displayName: 'Tax ID',
description: 'Tax ID (use List Taxes action to find IDs)',
required: false,
}),
text: Property.LongText({
displayName: 'Description',
description: 'Position description/text',
required: false,
}),
unit_price: Property.ShortText({
displayName: 'Unit Price',
description: 'Price per unit (max 6 decimals)',
required: false,
}),
discount_in_percent: Property.ShortText({
displayName: 'Discount (%)',
description: 'Discount percentage (max 6 decimals)',
required: false,
}),
parent_id: Property.Number({
displayName: 'Parent ID',
description: 'Parent position ID (for grouped positions)',
required: false,
}),
},
}),
};
},
}),
},
async run(context) {
const client = new BexioClient(context.auth);
const props = context.propsValue;
const requestBody: Record<string, unknown> = {
user_id: props['user_id'],
language_id: props['language_id'],
bank_account_id: props['bank_account_id'],
currency_id: props['currency_id'],
payment_type_id: props['payment_type_id'],
mwst_type: props['mwst_type'],
is_valid_from: props['is_valid_from'],
is_valid_until: props['is_valid_until'],
};
if (props['document_nr']) {
requestBody['document_nr'] = props['document_nr'];
}
if (props['title'] !== undefined) {
requestBody['title'] = props['title'] || null;
}
if (props['contact_id']) {
requestBody['contact_id'] = props['contact_id'];
}
if (props['contact_sub_id']) {
requestBody['contact_sub_id'] = props['contact_sub_id'];
}
if (props['pr_project_id']) {
requestBody['pr_project_id'] = props['pr_project_id'];
}
if (props['header']) {
requestBody['header'] = props['header'];
}
if (props['footer']) {
requestBody['footer'] = props['footer'];
}
if (props['mwst_is_net'] !== undefined) {
requestBody['mwst_is_net'] = props['mwst_is_net'];
}
if (props['show_position_taxes'] !== undefined) {
requestBody['show_position_taxes'] = props['show_position_taxes'];
}
if (props['contact_address_manual']) {
requestBody['contact_address_manual'] = props['contact_address_manual'];
}
if (props['delivery_address_type'] !== undefined) {
requestBody['delivery_address_type'] = props['delivery_address_type'];
}
if (props['delivery_address_manual_text']) {
requestBody['delivery_address_manual'] = props['delivery_address_manual_text'];
}
if (props['api_reference']) {
requestBody['api_reference'] = props['api_reference'];
}
if (props['viewed_by_client_at']) {
requestBody['viewed_by_client_at'] = props['viewed_by_client_at'];
}
if (props['kb_terms_of_payment_template_id']) {
requestBody['kb_terms_of_payment_template_id'] = props['kb_terms_of_payment_template_id'];
}
if (props['template_slug']) {
requestBody['template_slug'] = props['template_slug'];
}
const positionFields = props['positionFields'] as Record<string, unknown>;
if (positionFields && positionFields['positions'] && Array.isArray(positionFields['positions'])) {
requestBody['positions'] = (positionFields['positions'] as Array<Record<string, unknown>>).map((position) => {
const pos: Record<string, unknown> = {
type: position['type'] || 'KbPositionCustom',
};
if (position['amount']) {
pos['amount'] = position['amount'];
}
if (position['unit_id']) {
pos['unit_id'] = position['unit_id'];
}
if (position['account_id']) {
pos['account_id'] = position['account_id'];
}
if (position['tax_id']) {
pos['tax_id'] = position['tax_id'];
}
if (position['text']) {
pos['text'] = position['text'];
}
if (position['unit_price']) {
pos['unit_price'] = position['unit_price'];
}
if (position['discount_in_percent']) {
pos['discount_in_percent'] = position['discount_in_percent'];
}
if (position['parent_id']) {
pos['parent_id'] = position['parent_id'];
}
return pos;
});
}
const response = await client.post('/2.0/kb_offer', requestBody);
return response;
},
});