snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
---
title: جلب بيانات المنتج إلى Twenty
description: زامِن بيانات كتالوج المنتجات من مستودع بيانات إلى نظام إدارة علاقات العملاء (CRM) لديك وفق جدول زمني.
---
استخدم هذا النمط للحفاظ على مزامنة Twenty مع بيانات المنتجات من مستودع البيانات لديك (مثل Snowflake وBigQuery وPostgreSQL).
## بنية سير العمل
1. **المحفّز**: وفق جدول زمني
2. **Code**: الاستعلام عن مستودع البيانات لديك
3. **Code** (اختياري): تنسيق البيانات كمصفوفة
4. **Iterator**: التكرار عبر كل منتج
5. **Upsert Record**: أنشئ أو حدّث في Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## الخطوة 1: جدولة المُحفّز
اضبط سير العمل ليعمل بتواتر يتوافق مع احتياجاتك من حداثة البيانات:
* كل 5 دقائق لمزامنة شبه فورية
* كل ساعة للبيانات الأقل أهمية
* يوميًا للتحديثات الدفعية
## الخطوة 2: الاستعلام عن مستودع البيانات لديك
أضِف إجراء **Code** لجلب البيانات الحديثة:
```javascript
export const main = async () => {
const intervalMinutes = 10; // Match your schedule frequency
const cutoffTime = new Date(Date.now() - intervalMinutes * 60 * 1000).toISOString();
// Replace with your actual data warehouse connection
const response = await fetch("https://your-warehouse-api.com/query", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
query: `
SELECT id, name, sku, price, stock_quantity, updated_at
FROM products
WHERE updated_at >= '${cutoffTime}'
`
})
});
const data = await response.json();
return { products: data.results };
};
```
<Tip>
قم بالتصفية حسب `updated_at >= last X minutes` لاسترجاع السجلات التي تغيّرت حديثًا فقط. هذا يحافظ على كفاءة المزامنة.
</Tip>
## الخطوة 3: تنسيق البيانات (اختياري)
إذا كان مستودعك يُرجع البيانات بتنسيق يحتاج إلى تحويل، فأضف إجراء **Code** آخر. تشمل التحويلات الشائعة تحويل الأنواع، وإعادة تسمية الحقول، وتنظيف البيانات.
### مثال: بيانات المستخدم مع حقول منطقية وحقول الحالة
```javascript
export const main = async (params: {
users: any;
}): Promise<object> => {
const { users } = params;
const usersFormatted = typeof users === "string" ? JSON.parse(users) : users;
// Convert string "true"/"false" to actual booleans
const toBool = (v: any) => v === true || v === "true";
return {
users: usersFormatted.map((user) => ({
...user,
activityStatus: String(user.activityStatus).toUpperCase(),
isActiveLast30d: toBool(user.isActiveLast30d),
isActiveLast7d: toBool(user.isActiveLast7d),
isActiveLast24h: toBool(user.isActiveLast24h),
isTwenty: toBool(user.isTwenty),
})),
};
};
```
### مثال: بيانات المنتج مع تحويلات الأنواع
```javascript
export const main = async (params: { products: any }) => {
const products = typeof params.products === "string"
? JSON.parse(params.products)
: params.products;
return {
products: products.map(product => ({
externalId: product.id,
name: product.name,
sku: product.sku,
price: parseFloat(product.price), // String → Number
stockQuantity: parseInt(product.stock_quantity),
isActive: product.status === "active" // String → Boolean
}))
};
};
```
### مثال: تنسيق التاريخ والعملة
```javascript
export const main = async (params: { deals: any }) => {
const deals = typeof params.deals === "string"
? JSON.parse(params.deals)
: params.deals;
return {
deals: deals.map(deal => ({
...deal,
// Convert Unix timestamp to ISO date
closedAt: deal.closed_timestamp
? new Date(deal.closed_timestamp * 1000).toISOString()
: null,
// Ensure amount is a number (remove currency symbols)
amount: parseFloat(String(deal.amount).replace(/[^0-9.-]/g, "")),
// Normalize stage names
stage: deal.stage?.toLowerCase().replace(/_/g, " ")
}))
};
};
```
### التحويلات الشائعة
| تنسيق المصدر | تنسيق الهدف | كود |
| -------------------- | ---------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | تاريخ ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## الخطوة 4: التكرار عبر المنتجات
أضِف إجراء **Iterator**:
* المدخل: `{{code.products}}`
يقوم هذا بالتكرار عبر كل منتج في المصفوفة.
## الخطوة 5: إجراء Upsert لكل سجل
داخل المُكرِّر، أضف إجراء **Upsert Record**:
| الإعداد | القيمة |
| ---------------- | ------------------------------------ |
| **الكائن** | كائن المنتج المخصص لديك |
| **المطابقة حسب** | المعرّف الخارجي أو SKU (مُعرّف فريد) |
| **الاسم** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **السعر** | `{{iterator.item.price}}` |
<Tip>
استخدم **Upsert** (تحديث أو إنشاء) بدلًا من إنشاء فروع منفصلة للإنشاء مقابل التحديث. إنه أسرع في الإنشاء وأسهل في استكشاف الأخطاء وإصلاحها.
</Tip>
## أمثلة لحالات الاستخدام
| المصدر | بيانات |
| ---------------------------------- | ------------------------------------ |
| **نظام تخطيط موارد المؤسسة (ERP)** | كتالوج المنتجات، التسعير، المخزون |
| **منصة التجارة الإلكترونية** | الطلبات، العملاء، تحديثات المنتجات |
| **مستودع البيانات** | مقاييس مجمّعة، بيانات مُثرَاة |
| **نظام إدارة المخزون** | مستويات المخزون، تنبيهات إعادة الطلب |
## ذات صلة
* [مشغلات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-triggers)
* [إجراءات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-actions)
* [التعامل مع المصفوفات في إجراءات Code](/l/ar/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: جلب عمليات إرسال Typeform إلى Twenty
description: التعامل مع حمولة webhook الخاصة بـ Typeform لإنشاء عملاء محتملين من عمليات إرسال النماذج.
---
لإعداد webhook القياسي، راجع [إعداد مشغّل Webhook](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). يتناول هذا المقال المعالجة المحددة المطلوبة لبنية الحمولة المخصصة الخاصة بـ Typeform.
### الخطوة 1: إنشاء سير عمل Webhook
1. انتقل إلى **الإعدادات → سير العمل**
2. انقر **+ سير عمل جديد**
3. حدد **Webhook** كمشغّل
4. انسخ عنوان URL الخاص بالـ webhook
### الخطوة 2: تهيئة Typeform
1. في Typeform، افتح نموذجك
2. انتقل إلى **Connect → Webhooks**
3. الصق عنوان URL للـ webhook الخاص بـ Twenty
4. حفظ
### الخطوة 3: فهم حمولة Typeform
يرسل Typeform بنية JSON متداخلة. إليك مثال مبسط:
```json
{
"event_type": "form_response",
"form_response": {
"form_id": "abc123",
"submitted_at": "2025-01-15T10:30:00Z",
"answers": [
{
"text": "Jane",
"type": "text",
"field": { "id": "field1", "type": "short_text", "title": "First Name" }
},
{
"text": "Smith",
"type": "text",
"field": { "id": "field2", "type": "short_text", "title": "Last Name" }
},
{
"text": "Acme Corp",
"type": "text",
"field": { "id": "field3", "type": "short_text", "title": "Company" }
},
{
"email": "jane@acme.com",
"type": "email",
"field": { "id": "field4", "type": "email", "title": "Email" }
},
{
"type": "choice",
"field": { "id": "field5", "type": "dropdown", "title": "Team Size" },
"choice": { "label": "10-50" }
}
]
}
}
```
أمور رئيسية يجب ملاحظتها:
* بيانات النموذج متداخلة تحت `form_response`
* **تُعاد الإجابات كمصفوفة**، وليست كحقول مسماة
* تتضمن كل إجابة نوع الحقل وعنوانه للرجوع إليه
### الخطوة 4: استخراج الحقول من مصفوفة الإجابات
بما أن `answers` مصفوفة، يمكنك اختيار المصفوفة كاملة فقط في الخطوات اللاحقة — وليس حقول فردية. أضف إجراء **Code** لاستخراج الحقول التي تحتاجها:
```javascript
export const main = async (params: {
answers: any;
}): Promise<object> => {
const { answers } = params;
// Handle input that may come as a string or an array
const answersFormatted = typeof answers === "string"
? JSON.parse(answers)
: answers;
// Extract fields by position or by finding the field type
const firstName = answersFormatted[0]?.text || "";
const lastName = answersFormatted[1]?.text || "";
const company = answersFormatted[2]?.text || "";
const email = answersFormatted.find(a => a.type === "email")?.email || "";
const teamSize = answersFormatted.find(a => a.type === "choice")?.choice?.label || "";
return {
contact: {
firstName,
lastName,
company,
email,
teamSize
}
};
};
```
الآن في الخطوات اللاحقة، يمكنك تحديد `contact.firstName` و`contact.email` وغيرها من منتقي المتغيرات.
<Tip>
لمزيد من التفاصيل حول التعامل مع المصفوفات في إجراءات Code، راجع [التعامل مع المصفوفات في إجراءات Code](/l/ar/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### الخطوة 5: إنشاء السجل
أضف إجراء **Create Record**:
| الحقل | القيمة |
| --------------------- | ------------------------------------------------ |
| **الكائن** | الأشخاص |
| **الاسم الأول** | `{{code.contact.firstName}}` |
| **الاسم الأخير** | `{{code.contact.lastName}}` |
| **البريد الإلكتروني** | `{{code.contact.email}}` |
| **الشركة** | ابحث أو أنشئ بناء على `{{code.contact.company}}` |
### الخطوة 6: الاختبار والتفعيل
1. أرسل ردا تجريبيا في Typeform
2. تحقق من تشغيل سير العمل للتأكد من التقاط البيانات
3. فعّل سير العمل
## ذات صلة
* [إعداد مشغّل Webhook](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [التعامل مع المصفوفات في إجراءات Code](/l/ar/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: إنشاء ملف PDF من Twenty
description: أنشئ سير عمل لإنشاء ملف PDF (مثل عرض سعر) وإرفاقه بسجل.
---
أنشئ أو اجلب ملف PDF تلقائيًا وأرفقه بسجل في Twenty. يُستخدم هذا عادةً لإنشاء عروض أسعار أو فواتير أو تقارير مرتبطة بالشركات أو الفرص أو كائنات أخرى.
## نظرة عامة
يستخدم سير العمل هذا **المحفز اليدوي** بحيث يتمكن المستخدمون من إنشاء ملف PDF عند الطلب لأي سجل محدد. تتولى **الوظيفة المنطقية** ما يلي:
1. تنزيل ملف PDF من عنوان URL (من خدمة إنشاء PDF)
2. رفع الملف إلى Twenty
3. إنشاء مرفق مرتبط بالسجل
## المتطلبات الأساسية
قبل إعداد سير العمل:
1. **أنشئ مفتاح API**: انتقل إلى **الإعدادات → واجهات برمجة التطبيقات** ثم أنشئ مفتاح API جديدًا. ستحتاج إلى هذا الرمز لوظيفة المنطق.
2. **قم بإعداد خدمة إنشاء PDF** (اختياري): إذا كنت تريد إنشاء ملفات PDF ديناميكيًا (مثل عروض الأسعار)، فاستخدم خدمة مثل Carbone أو PDFMonkey أو DocuSeal لإنشاء ملف PDF والحصول على رابط تنزيل.
## إعداد خطوة بخطوة
### الخطوة 1: تهيئة المشغّل
1. انتقل إلى **سير العمل** وأنشئ سير عمل جديدًا
2. اختر **المحفز اليدوي**
3. اختر الكائن الذي تريد إرفاق ملفات PDF به (مثل **شركة** أو **فرصة**)
<Tip>
مع محفز يدوي، يمكن للمستخدمين تشغيل سير العمل هذا باستخدام زر يظهر في الجزء العلوي الأيمن بمجرد تحديد سجل - وهذا يعمل على تشغيل سير العمل لإنشاء وإرفاق PDF.
</Tip>
### الخطوة 2: إضافة وظيفة منطقية
1. إضافة إجراء **الكود** (الدالة المنطقية)
2. أنشئ وظيفة جديدة باستخدام الكود أدناه
3. قم بتهيئة معلمات الإدخال
#### معلمات الإدخال
| المعلمة | القيمة |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
إذا كنت تُرفِقه بكائن مختلف (شخص، فرصة، إلخ)، فأعد تسمية المعلمة وفقًا لذلك (مثلًا، `personId`، `opportunityId`) وحدّث الوظيفة المنطقية.
</Note>
#### كود الوظيفة المنطقية
```typescript
export const main = async (
params: { companyId: string },
) => {
const { companyId } = params;
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
// Cloud: https://api.twenty.com/graphql
// Self-hosted: https://your-domain.com/graphql
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
// Replace with your API key from Settings → APIs
const authToken = 'YOUR_API_KEY';
// Replace with your PDF URL
// This could be from a PDF generation service or a static URL
const pdfUrl = 'https://your-pdf-service.com/generated-quote.pdf';
const filename = 'quote.pdf';
// Step 1: Download the PDF file
const pdfResponse = await fetch(pdfUrl);
if (!pdfResponse.ok) {
throw new Error(`Failed to download PDF: ${pdfResponse.status}`);
}
const pdfBlob = await pdfResponse.blob();
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
const fieldMetadataIdQuery = `
query FindUploadFileFieldMetadataId {
objects {
edges {
node {
nameSingular
fieldsList {
id
name
}
}
}
}
}
`;
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
const response = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`
},
body: {
query: fieldMetadataIdQuery,
}
});
const result = await response.json();
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
// Step 3: Upload the file via GraphQL multipart upload
const uploadMutation = `
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
id
}
}
`;
const uploadForm = new FormData();
uploadForm.append('operations', JSON.stringify({
query: uploadMutation,
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
}));
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
uploadForm.append('0', pdfFile);
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${authToken}` },
body: uploadForm,
});
const uploadResult = await uploadResponse.json();
if (uploadResult.errors?.length) {
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
}
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
if (!fileId) {
throw new Error('No file id returned from upload');
}
// Step 4: Create the attachment linked to the company
const attachmentMutation = `
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
createAttachment(data: $data) {
id
name
}
}
`;
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: attachmentMutation,
variables: {
data: {
name: filename,
targetCompanyId: companyId,
file: [
{
fileId: fileId,
label: filename
}
]
},
},
}),
});
const attachmentResult = await attachmentResponse.json();
if (attachmentResult.errors?.length) {
throw new Error(`Attachment creation failed: ${attachmentResult.errors[0].message}`);
}
return attachmentResult.data?.createAttachment;
};
```
### الخطوة 3: خصّص وفق حالة الاستخدام الخاصة بك
#### لإرفاقه بكائن مختلف
استبدل `targetCompanyId` بالحقل المناسب:
| كائن | اسم الحقل |
| ---------- | -------------------------- |
| الشركة | `targetCompanyId` |
| شخص | `targetPersonId` |
| الفرصة | `targetOpportunityId` |
| كائن مخصّص | `targetYourCustomObjectId` |
حدّث كل من معلمة الوظيفة وكائن `variables.data` في عملية الـ mutation الخاصة بالمرفق.
#### لاستخدام رابط PDF ديناميكي
إذا كنت تستخدم خدمة إنشاء PDF، يمكنك:
1. أولًا، أنشئ إجراء طلب HTTP لإنشاء ملف PDF
2. مرّر رابط ملف PDF المُعاد إلى الوظيفة المنطقية كمعلمة
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### الخطوة 4: الاختبار والتفعيل
1. احفظ سير العمل
2. انتقل إلى سجل شركة
3. انقر على قائمة **⋮** واختر سير العمل الخاص بك
4. تحقّق من قسم **المرفقات** في السجل للتأكد من أنه تم إرفاق ملف PDF
5. فعّل سير العمل
## الدمج مع خدمات إنشاء ملفات PDF
لإنشاء عروض أسعار أو فواتير ديناميكية:
### مثال: إنشاء عرض سعر → إرفاق ملف PDF
| الخطوة | الإجراء | الغرض |
| ------ | ---------------------- | ---------------------------------------------------------- |
| 1 | المحفز اليدوي (الشركة) | يبدأ المستخدم التنفيذ على سجل |
| 2 | البحث عن سجل | الحصول على تفاصيل الفرصة أو بنود السطر |
| 3 | طلب HTTP | استدعِ واجهة برمجة تطبيقات إنشاء PDF باستخدام بيانات السجل |
| 4 | دالة منطقية | تنزيل وإرفاق ملف PDF المُنشأ |
### خدمات شائعة لإنشاء ملفات PDF
* **Carbone** - إنشاء مستندات يعتمد على القوالب
* **PDFMonkey** - إنشاء PDF ديناميكي من القوالب
* **DocuSeal** - منصة أتمتة المستندات
* **Documint** - إنشاء مستندات يعتمد على واجهة برمجة التطبيقات أولًا
توفر كل خدمة API الذي يعيد رابط PDF الذي يمكنك انتقاله إلى الدالة المنطقية.
## استكشاف الأخطاء وإصلاحها
| المشكلة | الحل |
| ------------------- | -------------------------------------------------------- |
| "فشل تنزيل ملف PDF" | تحقق من أن رابط ملف PDF قابل للوصول ويعيد ملف PDF صالحًا |
| "فشل الرفع" | تحقق من أن مفتاح API صالح ولديه أذونات كتابة |
| "فشل إنشاء المرفق" | تأكد من أن اسم حقل معرّف الكائن يطابق الكائن المستهدف |
## ذات صلة
* [مشغلات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-triggers)
* [الوظائف المنطقية](/l/ar/user-guide/workflows/capabilities/workflow-actions#code)
* [إنشاء عرض سعر أو فاتورة من Twenty](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: إنشاء عرض سعر أو فاتورة من Twenty
description: إنشاء فواتير تلقائياً في الأدوات الخارجية عند إغلاق الصفقات.
---
إرسال بيانات الصفقة تلقائياً إلى نظام الفوترة لديك (Stripe وQuickBooks وXero، إلخ) عند ربح فرصة.
## بنية سير العمل
1. **المحفّز**: يتم تحديث السجل (الفرصة)
2. **التصفية**: المرحلة = Closed Won
3. **Search Record**: الحصول على تفاصيل الشركة
4. **Code** (اختياري): تنسيق الحمولة
5. **HTTP Request**: الإرسال إلى نظام الفوترة
## الخطوة 1: إعداد المُحفّز
1. إنشاء سير عمل جديد
2. اختر المُحفّز **Record is Updated**
3. اختر **Opportunity** بوصفه الكائن
## الخطوة 2: التصفية من أجل Closed Won
أضف إجراء **Filter** للمتابعة فقط عند ربح الصفقة:
| الإعداد | القيمة |
| ---------- | ---------------------------------- |
| **الحقل** | المرحلة |
| **الشرط** | يساوي |
| **القيمة** | `CLOSED_WON` (أو اسم المرحلة لديك) |
<Tip>
يتم تشغيل المُحفّز عند أي تحديث للفرصة. تضمن التصفية أن يستمر سير العمل فقط عندما تتغير المرحلة إلى Closed Won.
</Tip>
## الخطوة 3: الحصول على تفاصيل الشركة
قد لا يتضمن سجل الفرصة جميع حقول الشركة التي تحتاجها للفاتورة. أضف إجراء **Search Record**:
| الإعداد | القيمة |
| ---------------- | -------------------------------------------- |
| **الكائن** | الشركة |
| **المطابقة حسب** | المعرّف يساوي `{{trigger.object.companyId}}` |
يؤدي ذلك إلى استرجاع سجل الشركة الكامل متضمناً عنوان الفوترة، ومعرّف الضريبة، وما إلى ذلك.
## الخطوة 4: تنسيق الحمولة (اختياري)
إذا كان نظام الفوترة لديك يتطلب تنسيقاً محدداً، فأضف إجراء **Code**:
```javascript
export const main = async (params: {
opportunity: any;
company: any;
}): Promise<object> => {
const { opportunity, company } = params;
return {
invoice: {
// Customer info from Company
customer_name: company.name,
customer_email: company.email || "",
billing_address: {
line1: company.address?.street || "",
city: company.address?.city || "",
postal_code: company.address?.postalCode || "",
country: company.address?.country || ""
},
tax_id: company.taxId || null,
// Invoice details from Opportunity
amount: opportunity.amount,
currency: opportunity.currency || "USD",
description: `Invoice for ${opportunity.name}`,
due_days: 30,
// Reference back to Twenty
metadata: {
opportunity_id: opportunity.id,
company_id: company.id
}
}
};
};
```
## الخطوة 5: الإرسال إلى نظام الفوترة
أضف إجراء **HTTP Request**:
| الإعداد | القيمة |
| ------------- | ------------------------------------------------ |
| **الطريقة** | POST |
| **عنوان URL** | نقطة نهاية واجهة برمجة تطبيقات الفوترة الخاصة بك |
| **الرؤوس** | `Authorization: Bearer YOUR_API_KEY` |
| **المحتوى** | `{{code.invoice}}` أو قم بربط الحقول مباشرةً |
### مثال: فاتورة Stripe
```
POST https://api.stripe.com/v1/invoices
Headers:
Authorization: Bearer sk_live_xxx
Content-Type: application/x-www-form-urlencoded
Body:
customer: {{company.stripeCustomerId}}
collection_method: send_invoice
days_until_due: 30
```
### مثال: فاتورة QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## الملخص الكامل لسير العمل
| الخطوة | الإجراء | الغرض |
| ------ | -------------------- | ----------------------------------------------- |
| 1 | المحفّز: تحديث السجل | يتم تشغيله عند حدوث أي تغيير في أي فرصة |
| ٢ | تصفية | المتابعة فقط إذا كانت المرحلة = Closed Won |
| 3 | البحث عن سجل | الحصول على تفاصيل الشركة الكاملة لأغراض الفوترة |
| 4 | كود | تنسيق البيانات لواجهة برمجة تطبيقات الفوترة |
| 5 | طلب HTTP | إنشاء فاتورة في نظام خارجي |
## نصائح
* **تخزين المعرّفات الخارجية**: احفظ معرّف الفاتورة الذي تُرجعه واجهة برمجة التطبيقات إلى الفرصة باستخدام إجراء **Update Record**
* **التعامل مع الأخطاء**: أضف فرعاً لإرسال إشعار إذا فشل طلب HTTP
* **اختبر أولاً**: استخدم وضع sandbox/test في نظام الفوترة قبل الإطلاق
## ذات صلة
* [إنشاء PDF من عشرين سنتي](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) - إرفاق ملفات PDFs التي تم إنشاؤها بالسجلات
* [مشغلات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-triggers)
* [إجراءات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-actions)
* [أتمتة Closed Won](/l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: إعداد مشغّل ربط الويب
description: استقبل بيانات من خدمات خارجية لتشغيل عمليات سير العمل.
---
تتيح مشغّلات ربط الويب للخدمات الخارجية بدء تشغيل عمليات سير العمل لديك عبر إرسال بيانات إلى عنوان URL فريد. استخدمها لربط النماذج والتطبيقات من جهات خارجية والتكاملات المخصّصة.
## متى تستخدم ربط الويب
| حالة الاستخدام | مثال |
| ----------------------- | ---------------------------------------------- |
| **نماذج الويب** | تُنشئ عمليات إرسال نموذج الاتصال عملاء محتملين |
| **تطبيقات جهات خارجية** | دفعة Stripe → إنشاء سجل عميل |
| **تكاملات مخصّصة** | تطبيقك → أتمتة Twenty |
| **أدوات بدون كود** | اتصالات Zapier وMake وn8n |
## إعداد خطوة بخطوة
### الخطوة 1: إنشاء سير العمل
1. اذهب إلى **الإعدادات → سير العمل**
2. انقر **+ سير عمل جديد**
3. قم بتسميته (مثلًا، "إرسال نموذج الموقع")
### الخطوة 2: تهيئة مشغّل ربط الويب
1. انقر على كتلة المشغّل
2. اختر **ربط الويب**
3. ستتلقى عنوان URL فريدًا لربط الويب مثل:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. انسخ هذا العنوان URL - ستحتاج إليه لخدمتك الخارجية
### الخطوة 3: تحديد بنية البيانات المتوقعة
بالنسبة لطلبات **POST**، حدِّد بنية جسم الطلب المتوقعة:
1. انقر **تحديد الجسم المتوقع**
2. أدخل JSON نموذجيًا يطابق ما سترسله خدمتك:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. انقر **حفظ** - سيؤدي هذا إلى إنشاء متغيّرات يمكنك استخدامها في الخطوات اللاحقة
### الخطوة 4: إضافة إجراءات
أضف الآن إجراءات تستخدم بيانات ربط الويب:
**مثال: إنشاء سجل شخص**
1. أضف إجراء **إنشاء سجل**
2. حدِّد كائن **الأشخاص**
3. عيّن الحقول:
| الحقل | القيمة |
| ----------------- | ------------------------------------------------- |
| الاسم الأول | `{{trigger.body.firstName}}` |
| الاسم الأخير | `{{trigger.body.lastName}}` |
| البريد الإلكتروني | `{{trigger.body.email}}` |
| الشركة | ابحث أو أنشئ بناءً على `{{trigger.body.company}}` |
### الخطوة 5: اختبار ربط الويب
قبل التفعيل، اختبر ربط الويب لديك:
**باستخدام cURL**:
```bash
curl -X POST https://api.twenty.com/webhooks/workflow/abc123... \
-H "Content-Type: application/json" \
-d '{"firstName":"Test","lastName":"User","email":"test@example.com"}'
```
**باستخدام Postman أو ما شابهه**:
1. أنشئ طلب POST إلى عنوان ربط الويب الخاص بك
2. عيّن ترويسة Content-Type إلى `application/json`
3. أضف جسم JSON الاختباري الخاص بك
4. أرسل وتحقّق من تشغيلات سير العمل
### الخطوة 6: التفعيل
بعد الاختبار، انقر **تفعيل** لجعل سير العمل فعّالًا.
## التعامل مع بُنى بيانات مختلفة
### بيانات متداخلة
إذا كان ربط الويب يرسل بيانات متداخلة:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
أشر إليها باستخدام: `{{trigger.body.contact.email}}`
### المصفوفات
إذا كانت البيانات تتضمن مصفوفات:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
تعتمد طريقة تعاملك مع المصفوفات على حالة الاستخدام لديك:
**عدد العناصر غير معروف → استخدم Iterator**
إذا كنت بحاجة إلى معالجة كل عنصر في المصفوفة (مثلًا، إنشاء سجل لكل عنصر)، أضف إجراء **Code** لتحليل المصفوفة، ثم استخدم **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
ثم استخدم Iterator للتكرار عبر: `{{code.items}}`
**حقول معروفة/محددة → استخرجها إلى حقول مسماة**
إذا كانت المصفوفة تحتوي على حقول محددة تريد الوصول إليها بشكل فردي (مثلًا، إجابات نموذج حيث الموضع 0 دائمًا هو "الاسم الأول"، والموضع 1 دائمًا هو "اسم العائلة")، فأضف إجراء **Code** لاستخراجها:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return {
product: {
name: items[0]?.name || "",
qty: items[0]?.qty || 0
}
};
};
```
يمكنك الآن اختيار `product.name` و`product.qty` بشكل فردي في الخطوات اللاحقة.
<Tip>
لمزيد من التفاصيل حول التعامل مع المصفوفات، راجع [التعامل مع المصفوفات في إجراءات Code](/l/ar/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## ذات صلة
* [مشغلات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-triggers)
* [إجراءات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-actions)