סקירה כללית
מודול החתימה הדיגיטלית מאפשר לאסוף חתימות אלקטרוניות על כל סוג של ישות במערכת TechLab Pro, כולל חשבוניות, חוזים, הצעות מחיר, תיקונים ועוד.
Polymorphic Design
חיבור לכל ישות במערכת דרך entity_type + entity_id
מספר חותמים
תמיכה בחתימות מרובות עם סדר חתימה מוגדר
חתימה חיצונית
שליחת קישור לחתימה ללקוחות ללא צורך בהרשמה
סטטיסטיקות המודול
תכונות עיקריות
סוגי חתימה
- חתימה מצוירת (drawn) - ציור על Canvas עם עכבר או מגע
- חתימה מודפסת (typed) - הקלדת שם בפונט חתימה
- תמונת חתימה (image) - העלאת תמונה של חתימה
- חתימה דיגיטלית (certificate) - חתימה עם תעודה דיגיטלית
ישויות נתמכות
- invoice - חשבונית
- contract - חוזה
- quote - הצעת מחיר
- repair - תיקון
- customer - לקוח
- document - מסמך
- project - פרויקט
- purchase_order - הזמנת רכש
תכונות מתקדמות
- תבניות חתימה מותאמות אישית
- סדר חתימות (parallel/sequential)
- תאריך תפוגה אוטומטי
- תזכורות אוטומטיות
- אימות חתימה ציבורי
- Hash verification
- לוג פעולות מלא (Audit)
- תמיכה ב-RTL (עברית)
- Responsive למובייל
מסד הנתונים
דיאגרמת ישויות (ERD)
┌─────────────────────────┐ ┌─────────────────────────┐
│ digital_signature_ │ │ digital_signature_ │
│ templates │────▶│ requests │
├─────────────────────────┤ ├─────────────────────────┤
│ id (PK) │ │ id (PK) │
│ template_name │ │ request_number (UNIQUE) │
│ template_code (UNIQUE) │ │ template_id (FK) │
│ entity_type │ │ entity_type ◀──────┐ │
│ required_signatures │ │ entity_id ◀──────┤ │
│ signature_order │ │ title │ │ Polymorphic
│ allow_drawn_signature │ │ status │ │ Relationship
│ ... │ │ customer_id (FK) │ │
└─────────────────────────┘ │ requested_by (FK) │ │
└─────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
│ digital_signature_ │ │ digital_signatures │ │ digital_signature_ │
│ signers │ │ │ │ audit │
├─────────────────────────┤ ├─────────────────────────┤ ├─────────────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ request_id (FK) │ │ signature_number (UNQ) │ │ request_id (FK) │
│ signer_name │──│ request_id (FK) │ │ signature_id (FK) │
│ signer_email │ │ signer_id (FK) │──│ entity_type │
│ signer_type │ │ entity_type │ │ entity_id │
│ signing_order │ │ entity_id │ │ action │
│ status │ │ signature_type │ │ actor_type │
│ access_token │ │ signature_data (Base64) │ │ ip_address │
│ signature_id (FK) │◀─│ status │ │ created_at │
└─────────────────────────┘ │ ip_address │ └─────────────────────────┘
│ signed_at │
└─────────────────────────┘
טבלת digital_signature_templates
תבניות חתימה מגדירות את הדרישות לחתימה עבור כל סוג ישות.
| עמודה | סוג | תיאור |
|---|---|---|
id |
INTEGER PK | מזהה ייחודי |
template_name |
VARCHAR(100) | שם התבנית |
template_code |
VARCHAR(50) UNIQUE | קוד ייחודי (INV_APPROVAL) |
entity_type |
VARCHAR(50) | סוג הישות (invoice, contract...) |
required_signatures_count |
INTEGER | מספר חתימות נדרשות |
signature_order |
VARCHAR(20) | סדר חתימות: any / sequential |
allow_drawn_signature |
BOOLEAN | האם לאפשר חתימה מצוירת |
signature_valid_days |
INTEGER | תוקף החתימה בימים (365) |
request_expires_hours |
INTEGER | תפוגת בקשה בשעות (72) |
טבלת digital_signature_requests
בקשות לחתימה - מייצגות בקשה ספציפית לחתום על ישות.
| עמודה | סוג | תיאור |
|---|---|---|
request_number |
VARCHAR(50) UNIQUE | מספר בקשה: SIG-REQ-2025-00001 |
entity_type |
VARCHAR(50) | Polymorphic: סוג הישות |
entity_id |
INTEGER | Polymorphic: מזהה הישות |
status |
VARCHAR(30) | pending, sent, partially_signed, completed, expired, cancelled |
access_token |
VARCHAR(128) | Token לגישה חיצונית |
אינדקסים
-- אינדקס לשאילתות פולימורפיות CREATE INDEX idx_signature_request_entity ON digital_signature_requests (entity_type, entity_id); CREATE INDEX idx_digital_signature_entity ON digital_signatures (entity_type, entity_id); CREATE INDEX idx_signature_audit_entity ON digital_signature_audit (entity_type, entity_id); -- אינדקס ללוג פעולות CREATE INDEX idx_signature_audit_request ON digital_signature_audit (request_id); CREATE INDEX idx_signature_audit_action ON digital_signature_audit (action);
מודלים (Models)
DigitalSignatureTemplate
תבנית המגדירה את דרישות החתימה לסוג ישות מסוים.
class DigitalSignatureTemplate(db.Model):
__tablename__ = 'digital_signature_templates'
id = db.Column(db.Integer, primary_key=True)
template_name = db.Column(db.String(100), nullable=False)
template_code = db.Column(db.String(50), unique=True, nullable=False)
entity_type = db.Column(db.String(50), nullable=False)
# Signature Requirements
required_signatures_count = db.Column(db.Integer, default=1)
signature_order = db.Column(db.String(20), default='any') # any, sequential
# Allowed Signature Types
allow_drawn_signature = db.Column(db.Boolean, default=True)
allow_typed_signature = db.Column(db.Boolean, default=True)
allow_image_signature = db.Column(db.Boolean, default=True)
# Methods
def to_dict(self) -> dict
DigitalSignatureRequest
בקשה לחתימה על ישות ספציפית - הליבה של המודול.
class DigitalSignatureRequest(db.Model):
__tablename__ = 'digital_signature_requests'
# Polymorphic Relationship - התחברות לכל ישות
entity_type = db.Column(db.String(50), nullable=False) # invoice, contract, repair...
entity_id = db.Column(db.Integer, nullable=False) # ID of the entity
# Composite Index for fast lookups
__table_args__ = (
db.Index('idx_signature_request_entity', 'entity_type', 'entity_id'),
)
# Properties
@property
def is_expired(self) -> bool
@property
def completion_percentage(self) -> int
# Methods
def generate_access_token(self, expires_hours=72) -> str
def to_dict(self, include_signatures=False, include_signers=False) -> dict
DigitalSignature
החתימה עצמה - מכילה את נתוני החתימה ומידע על החותם.
class DigitalSignature(db.Model):
__tablename__ = 'digital_signatures'
signature_number = db.Column(db.String(50), unique=True) # SIG-2025-00001
signature_type = db.Column(db.String(30)) # drawn, typed, image, certificate
signature_data = db.Column(db.Text) # Base64 encoded signature
signature_hash = db.Column(db.String(128)) # SHA-512 for verification
# Signer Information
signer_name = db.Column(db.String(200), nullable=False)
signer_email = db.Column(db.String(200))
signer_type = db.Column(db.String(30)) # customer, company, witness
# Legal Information
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.String(500))
geolocation = db.Column(db.JSON)
consent_given = db.Column(db.Boolean, default=True)
# Methods
def generate_hash(self, document_hash=None) -> str
def verify_hash(self, document_hash=None) -> bool
Helper Functions
# יצירת מספר בקשה ייחודי
def generate_signature_request_number() -> str:
# Returns: SIG-REQ-2025-00001
# יצירת מספר חתימה ייחודי
def generate_signature_number() -> str:
# Returns: SIG-2025-00001
# קבלת חתימות לישות
def get_entity_signatures(entity_type: str, entity_id: int) -> List[DigitalSignature]
# קבלת בקשות חתימה לישות
def get_entity_signature_requests(entity_type: str, entity_id: int) -> List[DigitalSignatureRequest]
# יצירת רשומת Audit
def create_audit_log(request_id=None, action=None, ...) -> DigitalSignatureAudit
API Reference
כל ה-API endpoints נמצאים תחת /api/digital-signature/
Templates API
/api/digital-signature/templates
קבלת רשימת תבניות חתימה
Query params: entity_type, is_active, search, page, per_page/api/digital-signature/templates
יצירת תבנית חדשה
Required: template_name, template_code, entity_type/api/digital-signature/templates/{id}
עדכון תבנית
Signature Requests API
/api/digital-signature/requests
קבלת רשימת בקשות חתימה
Query params: entity_type, entity_id, status, customer_id, priority, search/api/digital-signature/requests
יצירת בקשת חתימה חדשה
{
"entity_type": "invoice",
"entity_id": 123,
"title": "אישור חשבונית #123",
"description": "נא לחתום על החשבונית המצורפת",
"template_id": 1,
"priority": "normal",
"signers": [
{
"signer_name": "ישראל ישראלי",
"signer_email": "israel@example.com",
"signer_type": "customer"
}
]
}
/api/digital-signature/requests/{id}/send
שליחת בקשה לחתימה (email/sms)
/api/digital-signature/requests/{id}/cancel
ביטול בקשת חתימה
Signatures API
/api/digital-signature/sign
ביצוע חתימה
{
"request_id": 1,
"signer_id": 1,
"signature_type": "drawn",
"signature_data": "data:image/png;base64,iVBORw0...",
"signer_name": "ישראל ישראלי",
"signer_email": "israel@example.com",
"consent_given": true,
"consent_text": "אני מאשר/ת שקראתי והבנתי את המסמך"
}
/api/digital-signature/signatures/{id}/revoke
ביטול חתימה
Entity-Based API (Polymorphic)
/api/digital-signature/entity/{type}/{id}/signatures
קבלת כל החתימות לישות מסוימת
/api/digital-signature/entity/{type}/{id}/requests
קבלת כל בקשות החתימה לישות
/api/digital-signature/entity/{type}/{id}/request
יצירת בקשת חתימה לישות
Public Endpoints (ללא אימות)
/api/digital-signature/verify/{signature_number}
אימות חתימה ציבורי
// Response
{
"valid": true,
"signature_number": "SIG-2025-00001",
"signer_name": "ישראל ישראלי",
"signed_at": "2025-12-01T14:30:00",
"signature_type": "drawn",
"entity_type": "invoice"
}
/api/digital-signature/constants
קבלת קבועים (entity_types, statuses)
/api/digital-signature/signing-page/{token}
קבלת נתוני דף חתימה ציבורי
Use Cases
Use Case 1: חתימה על חשבונית
תרחיש: לקוח צריך לאשר חשבונית לפני שליחה
- יצירת בקשת חתימה לחשבונית #123
- הגדרת הלקוח כחותם
- שליחת קישור חתימה במייל
- הלקוח פותח את הקישור וחותם
- החשבונית מסומנת כ"מאושרת"
// יצירת בקשת חתימה לחשבונית
const response = await fetch('/api/digital-signature/requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entity_type: 'invoice',
entity_id: 123,
title: 'אישור חשבונית #123',
signers: [{
signer_name: 'ישראל ישראלי',
signer_email: 'customer@example.com',
signer_type: 'customer'
}]
})
});
Use Case 2: חתימה על חוזה עם מספר צדדים
תרחיש: חוזה שירות דורש חתימת לקוח + נציג חברה
- יצירת תבנית "חוזה שירות" עם 2 חתימות
- יצירת בקשת חתימה עם 2 חותמים
- הגדרת סדר חתימות (לקוח ראשון)
- שליחה ללקוח
- לאחר חתימת הלקוח - שליחה לנציג החברה
- החוזה מושלם כשכולם חתמו
// יצירת בקשה עם מספר חותמים
const response = await fetch('/api/digital-signature/requests', {
method: 'POST',
body: JSON.stringify({
entity_type: 'contract',
entity_id: 456,
title: 'חוזה שירות שנתי',
template_id: 2, // תבנית חוזה
signers: [
{
signer_name: 'ישראל ישראלי',
signer_email: 'customer@example.com',
signer_type: 'customer',
signing_order: 1
},
{
signer_name: 'מנהל החברה',
signer_email: 'manager@company.com',
signer_type: 'company',
signing_order: 2
}
]
})
});
Use Case 3: אישור קבלת תיקון
תרחיש: לקוח מאשר קבלת מכשיר מתוקן
- כשהתיקון מוכן, נוצרת בקשת חתימה אוטומטית
- הלקוח מגיע לאסוף את המכשיר
- חותם על טאבלט בחנות
- מקבל SMS עם קישור לאימות
Use Case 4: אימות חתימה חיצוני
תרחיש: צד שלישי רוצה לוודא תקינות חתימה
// אימות חתימה ציבורי (ללא התחברות)
const response = await fetch('/api/digital-signature/verify/SIG-2025-00001');
const result = await response.json();
if (result.valid) {
console.log(`חתימה תקינה של: ${result.signer_name}`);
console.log(`נחתם בתאריך: ${result.signed_at}`);
} else {
console.log(`חתימה לא תקינה: ${result.reason_he}`);
}
Workflows
תהליך חתימה סטנדרטי
יצירת בקשת חתימה
משתמש מורשה יוצר בקשה לחתימה על ישות (חשבונית, חוזה וכו')
POST /api/digital-signature/requests
הוספת חותמים
הגדרת מי צריך לחתום (לקוח, נציג חברה, עד)
POST /api/digital-signature/requests/{id}/signers
שליחת בקשה
שליחת הבקשה בדוא"ל/SMS עם קישור לחתימה
POST /api/digital-signature/requests/{id}/send
חתימה על ידי החותם
החותם פותח את הקישור, קורא את המסמך וחותם
POST /api/digital-signature/sign
השלמה ואימות
כל החותמים חתמו - הבקשה מושלמת. ניתן לאמת בכל עת.
GET /api/digital-signature/verify/{signature_number}
סטטוסים של בקשת חתימה
┌─────────┐ ┌──────┐ ┌─────────────────┐ ┌───────────┐
│ pending │────▶│ sent │────▶│ partially_signed│────▶│ completed │
└─────────┘ └──────┘ └─────────────────┘ └───────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌───────────┐ ┌─────────┐ ┌──────────┐
│ cancelled │ │ expired │ │ rejected │
└───────────┘ └─────────┘ └──────────┘
אינטגרציה עם מודולים אחרים
אינטגרציה עם מודול חשבוניות
# בקובץ api_invoicing.py - לאחר יצירת חשבונית
from app.models_digital_signature import (
DigitalSignatureRequest,
generate_signature_request_number
)
@invoicing_bp.route('/invoices/<int:id>/request-signature', methods=['POST'])
@login_required
def request_invoice_signature(id):
"""בקשת חתימה על חשבונית"""
invoice = Invoice.query.get_or_404(id)
# יצירת בקשת חתימה
sig_request = DigitalSignatureRequest(
request_number=generate_signature_request_number(),
entity_type='invoice',
entity_id=invoice.id,
title=f'אישור חשבונית #{invoice.invoice_number}',
customer_id=invoice.customer_id,
requested_by=current_user.id
)
db.session.add(sig_request)
db.session.commit()
return jsonify(sig_request.to_dict())
אינטגרציה עם מודול תיקונים
# בקובץ api_routes.py - כשתיקון מושלם
@api_bp.route('/repairs/<int:id>/complete', methods=['POST'])
@login_required
def complete_repair(id):
"""השלמת תיקון ובקשת חתימה"""
repair = Repair.query.get_or_404(id)
repair.status = 'completed'
# יצירת בקשת חתימה אוטומטית
sig_request = DigitalSignatureRequest(
request_number=generate_signature_request_number(),
entity_type='repair',
entity_id=repair.id,
title=f'אישור קבלת תיקון #{repair.repair_number}',
customer_id=repair.customer_id,
requested_by=current_user.id
)
# הוספת הלקוח כחותם
signer = DigitalSignatureSigner(
request=sig_request,
signer_name=repair.customer.name,
signer_email=repair.customer.email,
signer_phone=repair.customer.phone,
signer_type='customer'
)
db.session.add_all([sig_request, signer])
db.session.commit()
return jsonify({'repair': repair.to_dict(), 'signature_request': sig_request.to_dict()})
שאילתות פולימורפיות
# קבלת כל החתימות לישות מסוימת
from app.models_digital_signature import get_entity_signatures, get_entity_signature_requests
# בכל מקום באפליקציה
invoice_signatures = get_entity_signatures('invoice', 123)
contract_signatures = get_entity_signatures('contract', 456)
# או עם שאילתה ישירה
signatures = DigitalSignature.query.filter_by(
entity_type='invoice',
entity_id=123,
status='completed'
).all()
אבטחה
אימות וגישה
הרשאות API
- Endpoints מוגנים: רוב ה-endpoints דורשים התחברות (
@login_required) - Endpoints ציבוריים: verify, constants, signing-page - ללא אימות
- Access Tokens: קישורי חתימה חיצוניים משתמשים ב-tokens חד-פעמיים עם תפוגה
אימות חתימות
Hash Verification
- כל חתימה מקבלת hash ייחודי (SHA-512)
- ה-hash כולל: signature_data + document_hash + timestamp + signer_name
- ניתן לאמת תקינות חתימה בכל עת
def generate_hash(self, document_hash=None):
hash_input = f"{self.signature_data}{document_hash}{self.signed_at}{self.signer_name}"
return hashlib.sha512(hash_input.encode()).hexdigest()
def verify_hash(self, document_hash=None):
expected_hash = self.generate_hash(document_hash)
return self.signature_hash == expected_hash
מידע משפטי
Legal Compliance
- IP Address: נשמרת כתובת IP של החותם
- User Agent: נשמר מידע על הדפדפן/מכשיר
- Timestamp: זמן חתימה מדויק
- Consent: אישור מפורש שהחותם קרא והבין
- Audit Log: לוג מלא של כל הפעולות
דוגמאות קוד
JavaScript - יצירת בקשת חתימה
// יצירת בקשת חתימה מלאה
async function createSignatureRequest(entityType, entityId, title, signers) {
try {
const response = await fetch('/api/digital-signature/requests', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
entity_type: entityType,
entity_id: entityId,
title: title,
priority: 'normal',
signers: signers
})
});
if (!response.ok) {
throw new Error('Failed to create request');
}
const data = await response.json();
console.log('Created request:', data.request_number);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// שימוש
createSignatureRequest('invoice', 123, 'אישור חשבונית', [
{
signer_name: 'ישראל ישראלי',
signer_email: 'israel@example.com',
signer_type: 'customer'
}
]);
JavaScript - Canvas Signature
// איתחול Canvas לחתימה
class SignatureCanvas {
constructor(canvasElement) {
this.canvas = canvasElement;
this.ctx = canvas.getContext('2d');
this.isDrawing = false;
// הגדרות
this.ctx.strokeStyle = '#000';
this.ctx.lineWidth = 2;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.setupEvents();
}
setupEvents() {
// Mouse events
this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
this.canvas.addEventListener('mousemove', (e) => this.draw(e));
this.canvas.addEventListener('mouseup', () => this.stopDrawing());
this.canvas.addEventListener('mouseout', () => this.stopDrawing());
// Touch events
this.canvas.addEventListener('touchstart', (e) => this.startDrawing(e));
this.canvas.addEventListener('touchmove', (e) => this.draw(e));
this.canvas.addEventListener('touchend', () => this.stopDrawing());
}
startDrawing(e) {
this.isDrawing = true;
const pos = this.getPosition(e);
this.ctx.beginPath();
this.ctx.moveTo(pos.x, pos.y);
}
draw(e) {
if (!this.isDrawing) return;
e.preventDefault();
const pos = this.getPosition(e);
this.ctx.lineTo(pos.x, pos.y);
this.ctx.stroke();
}
stopDrawing() {
this.isDrawing = false;
}
getPosition(e) {
const rect = this.canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
getSignatureData() {
return this.canvas.toDataURL('image/png');
}
isEmpty() {
const blank = document.createElement('canvas');
blank.width = this.canvas.width;
blank.height = this.canvas.height;
return this.canvas.toDataURL() === blank.toDataURL();
}
}
// שימוש
const sigCanvas = new SignatureCanvas(document.getElementById('signatureCanvas'));
// שליחת החתימה
async function submitSignature() {
if (sigCanvas.isEmpty()) {
alert('נא לחתום לפני השליחה');
return;
}
const signatureData = sigCanvas.getSignatureData();
await fetch('/api/digital-signature/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: currentRequestId,
signature_type: 'drawn',
signature_data: signatureData,
signer_name: 'ישראל ישראלי',
consent_given: true
})
});
}
Python - אינטגרציה בקוד Backend
from app.models_digital_signature import (
DigitalSignatureRequest, DigitalSignatureSigner, DigitalSignature,
generate_signature_request_number, create_audit_log
)
from app import db
from datetime import datetime, timedelta
def create_signature_request_for_entity(entity_type, entity_id, title, signers_info, requested_by_user):
"""
יצירת בקשת חתימה לישות
Args:
entity_type: סוג הישות (invoice, contract, etc.)
entity_id: מזהה הישות
title: כותרת הבקשה
signers_info: רשימת מילונים עם פרטי החותמים
requested_by_user: אובייקט User של המבקש
Returns:
DigitalSignatureRequest: אובייקט הבקשה שנוצרה
"""
# יצירת הבקשה
sig_request = DigitalSignatureRequest(
request_number=generate_signature_request_number(),
entity_type=entity_type,
entity_id=entity_id,
title=title,
status='pending',
expires_at=datetime.utcnow() + timedelta(hours=72),
requested_by=requested_by_user.id
)
db.session.add(sig_request)
db.session.flush() # קבלת ID
# הוספת חותמים
for idx, signer_info in enumerate(signers_info):
signer = DigitalSignatureSigner(
request_id=sig_request.id,
signer_name=signer_info['name'],
signer_email=signer_info.get('email'),
signer_phone=signer_info.get('phone'),
signer_type=signer_info.get('type', 'customer'),
signing_order=idx + 1
)
signer.generate_access_token(72) # Token לשעות 72
db.session.add(signer)
# יצירת רשומת audit
create_audit_log(
request_id=sig_request.id,
entity_type=entity_type,
entity_id=entity_id,
action='created',
action_details=f'Signature request created: {title}',
actor_type='user',
actor_id=requested_by_user.id,
actor_name=requested_by_user.username
)
db.session.commit()
return sig_request
# שימוש לדוגמה
request = create_signature_request_for_entity(
entity_type='invoice',
entity_id=123,
title='אישור חשבונית #INV-2025-123',
signers_info=[
{'name': 'ישראל ישראלי', 'email': 'israel@example.com', 'type': 'customer'},
{'name': 'מנהל חברה', 'email': 'manager@company.com', 'type': 'company'}
],
requested_by_user=current_user
)