feat(协议签署): 接入供应商代发、代销和生产合作合同模板 (#306)
This commit is contained in:
parent
d8dfb33437
commit
50cfbf1c14
@ -11,6 +11,7 @@ use App\Support\CourierAgreementFileArchive;
|
|||||||
use App\Support\CourierAgreementSignatureService;
|
use App\Support\CourierAgreementSignatureService;
|
||||||
use App\Support\CourierQuoteConfirmation;
|
use App\Support\CourierQuoteConfirmation;
|
||||||
use App\Support\CourierQuoteSignatureService;
|
use App\Support\CourierQuoteSignatureService;
|
||||||
|
use App\Support\SupplierCooperationTemplates;
|
||||||
use App\Utilities\SystemAccess;
|
use App\Utilities\SystemAccess;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@ -19,6 +20,7 @@ use Illuminate\Routing\Controller;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
@ -41,12 +43,10 @@ class CourierAgreementSignatures extends Controller
|
|||||||
$request->validate(['partner_type' => ['required', 'in:courier,supplier']]);
|
$request->validate(['partner_type' => ['required', 'in:courier,supplier']]);
|
||||||
if ($request->input('partner_type') === 'supplier') {
|
if ($request->input('partner_type') === 'supplier') {
|
||||||
if ($request->input('document_type') === 'cooperation-contract') {
|
if ($request->input('document_type') === 'cooperation-contract') {
|
||||||
throw ValidationException::withMessages([
|
$request->validate(['supplier_contract_template' => ['required', Rule::in(array_keys(SupplierCooperationTemplates::TEMPLATES))]]);
|
||||||
'document_type' => '供应商合作合同模板尚未提供,暂不能发起签署。',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
$request->validate(['supplier_id' => ['required', 'integer', 'min:1'],
|
$request->validate(['supplier_id' => ['required', 'integer', 'min:1'],
|
||||||
'document_type' => ['required', 'in:account-proof,account-change,entity-change,authorization']]);
|
'document_type' => ['required', 'in:account-proof,account-change,entity-change,authorization,cooperation-contract']]);
|
||||||
$supplier = app(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class)
|
$supplier = app(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class)
|
||||||
->signingSupplier((int) $request->input('supplier_id'));
|
->signingSupplier((int) $request->input('supplier_id'));
|
||||||
if (! $supplier) throw ValidationException::withMessages(['supplier_id' => '供应商资料暂不可用,请重新选择供应商。']);
|
if (! $supplier) throw ValidationException::withMessages(['supplier_id' => '供应商资料暂不可用,请重新选择供应商。']);
|
||||||
@ -65,7 +65,7 @@ class CourierAgreementSignatures extends Controller
|
|||||||
abort_unless(Schema::hasTable('business_courier_agreement_signatures'), 503, '协议签署数据表尚未完成迁移,请先执行数据库迁移。');
|
abort_unless(Schema::hasTable('business_courier_agreement_signatures'), 503, '协议签署数据表尚未完成迁移,请先执行数据库迁移。');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->input('document_type') === 'cooperation-contract') {
|
if ($request->input('document_type') === 'cooperation-contract' && $request->input('partner_type') === 'courier') {
|
||||||
// 合作合同统一由我方固定 E 签宝账号完成第二签署方签署。
|
// 合作合同统一由我方固定 E 签宝账号完成第二签署方签署。
|
||||||
$request->merge([
|
$request->merge([
|
||||||
'secondary_esign_account' => self::COOPERATION_SECONDARY_ESIGN_ACCOUNT,
|
'secondary_esign_account' => self::COOPERATION_SECONDARY_ESIGN_ACCOUNT,
|
||||||
@ -149,7 +149,8 @@ class CourierAgreementSignatures extends Controller
|
|||||||
if ($data['counterparty_signer_type'] === 'person') {
|
if ($data['counterparty_signer_type'] === 'person') {
|
||||||
$data['operator_name'] = trim((string) $data['cooperation_party_b_name']);
|
$data['operator_name'] = trim((string) $data['cooperation_party_b_name']);
|
||||||
} elseif (blank($data['operator_name'] ?? null)) {
|
} elseif (blank($data['operator_name'] ?? null)) {
|
||||||
throw ValidationException::withMessages(['operator_name' => '请填写快递方企业经办人的 E 签宝实名认证姓名。']);
|
$party = $data['partner_type'] === 'supplier' ? '供应商' : '快递方';
|
||||||
|
throw ValidationException::withMessages(['operator_name' => "请填写{$party}企业经办人的 E 签宝实名认证姓名。"]);
|
||||||
}
|
}
|
||||||
if (blank($data['secondary_operator_name'] ?? null)) {
|
if (blank($data['secondary_operator_name'] ?? null)) {
|
||||||
throw ValidationException::withMessages(['secondary_operator_name' => '请填写我方 E 签宝账号绑定的实名认证姓名。']);
|
throw ValidationException::withMessages(['secondary_operator_name' => '请填写我方 E 签宝账号绑定的实名认证姓名。']);
|
||||||
@ -158,7 +159,7 @@ class CourierAgreementSignatures extends Controller
|
|||||||
if ($data['counterparty_signer_type'] === 'organization') {
|
if ($data['counterparty_signer_type'] === 'organization') {
|
||||||
$companyNames[] = trim($data['cooperation_party_b_name']);
|
$companyNames[] = trim($data['cooperation_party_b_name']);
|
||||||
}
|
}
|
||||||
foreach (['operator_name' => '快递方', 'secondary_operator_name' => '我方'] as $field => $party) {
|
foreach (['operator_name' => $data['partner_type'] === 'supplier' ? '供应商' : '快递方', 'secondary_operator_name' => '我方'] as $field => $party) {
|
||||||
$data[$field] = trim($data[$field]);
|
$data[$field] = trim($data[$field]);
|
||||||
if (in_array($data[$field], $companyNames, true)) {
|
if (in_array($data[$field], $companyNames, true)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@ -446,7 +447,21 @@ class CourierAgreementSignatures extends Controller
|
|||||||
{
|
{
|
||||||
$date = ['required', 'date', 'after_or_equal:today', 'before_or_equal:' . now()->addDays(90)->toDateString()];
|
$date = ['required', 'date', 'after_or_equal:today', 'before_or_equal:' . now()->addDays(90)->toDateString()];
|
||||||
|
|
||||||
|
$supplierContract = request('partner_type') === 'supplier' && request('document_type') === 'cooperation-contract';
|
||||||
|
$supplierFields = [];
|
||||||
|
foreach (SupplierCooperationTemplates::FIELDS as $key => $field) {
|
||||||
|
$supplierFields['supplier_contract_fields.' . $key] = [
|
||||||
|
Rule::requiredIf($supplierContract && ($field['required'] ?? false)), 'nullable', 'string',
|
||||||
|
'max:' . ($key === 'supply_price' ? 1000 : 255),
|
||||||
|
];
|
||||||
|
if (($field['type'] ?? '') === 'email') {
|
||||||
|
$supplierFields['supplier_contract_fields.' . $key][] = 'email';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'supplier_contract_template' => [Rule::requiredIf($supplierContract), 'nullable', Rule::in(array_keys(SupplierCooperationTemplates::TEMPLATES)), Rule::prohibitedIf(! $supplierContract)],
|
||||||
|
'supplier_contract_fields' => ['nullable', 'array:' . implode(',', array_keys(SupplierCooperationTemplates::FIELDS))],
|
||||||
'partner_type' => ['required', 'in:courier,supplier'],
|
'partner_type' => ['required', 'in:courier,supplier'],
|
||||||
'supplier_id' => ['required_if:partner_type,supplier', 'nullable', 'integer', 'min:1'],
|
'supplier_id' => ['required_if:partner_type,supplier', 'nullable', 'integer', 'min:1'],
|
||||||
'idempotency_key' => ['required', 'string', 'max:80', 'regex:/^[A-Za-z0-9_-]+$/'],
|
'idempotency_key' => ['required', 'string', 'max:80', 'regex:/^[A-Za-z0-9_-]+$/'],
|
||||||
@ -504,7 +519,7 @@ class CourierAgreementSignatures extends Controller
|
|||||||
'payment_counterparty' => ['required_if:document_type,authorization', 'string', 'max:255'],
|
'payment_counterparty' => ['required_if:document_type,authorization', 'string', 'max:255'],
|
||||||
'authorization_start_date' => ['required_if:document_type,authorization', 'date'],
|
'authorization_start_date' => ['required_if:document_type,authorization', 'date'],
|
||||||
'authorization_end_date' => ['required_if:document_type,authorization', 'date', 'after_or_equal:authorization_start_date'],
|
'authorization_end_date' => ['required_if:document_type,authorization', 'date', 'after_or_equal:authorization_start_date'],
|
||||||
];
|
] + $supplierFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function payload(array $data): array
|
private function payload(array $data): array
|
||||||
@ -517,6 +532,9 @@ class CourierAgreementSignatures extends Controller
|
|||||||
$payload['former_legal_representative'] = trim((string) ($data['operator_name'] ?? ''));
|
$payload['former_legal_representative'] = trim((string) ($data['operator_name'] ?? ''));
|
||||||
$payload['entity_proof_date'] = $data['signature_date'] ?? null;
|
$payload['entity_proof_date'] = $data['signature_date'] ?? null;
|
||||||
}
|
}
|
||||||
|
if (($data['partner_type'] ?? 'courier') === 'supplier' && ($data['document_type'] ?? '') === 'cooperation-contract') {
|
||||||
|
$payload['supplier_contract_signer_name'] = $data['operator_name'];
|
||||||
|
}
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,11 @@ class CourierAgreementSignature extends Model
|
|||||||
|
|
||||||
public function documentLabel(): string
|
public function documentLabel(): string
|
||||||
{
|
{
|
||||||
|
foreach (\App\Support\SupplierCooperationTemplates::TEMPLATES as $template) {
|
||||||
|
if ($this->template_code === $template['code']) {
|
||||||
|
return $template['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
return self::DOCUMENTS[$this->document_type]['name'] ?? $this->document_type;
|
return self::DOCUMENTS[$this->document_type]['name'] ?? $this->document_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,8 @@ final class CourierAgreementPdf
|
|||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
$signatureDate = '待填写';
|
$signatureDate = '待填写';
|
||||||
}
|
}
|
||||||
$html = view('business.pdf.courier-agreement', [
|
$supplierContract = app(SupplierCooperationTemplates::class)->isSupplierContract($signature);
|
||||||
|
$html = view($supplierContract ? 'business.pdf.supplier-cooperation-contract' : 'business.pdf.courier-agreement', [
|
||||||
'signature' => $signature,
|
'signature' => $signature,
|
||||||
'type' => $signature->document_type,
|
'type' => $signature->document_type,
|
||||||
'title' => $this->title($signature),
|
'title' => $this->title($signature),
|
||||||
|
|||||||
@ -23,6 +23,9 @@ final class CourierAgreementSignatureService
|
|||||||
abort_if(! EsignClient::configured(), 422, 'E 签宝沙箱尚未配置 App ID 或 AppSecret。');
|
abort_if(! EsignClient::configured(), 422, 'E 签宝沙箱尚未配置 App ID 或 AppSecret。');
|
||||||
$companyId = (int) (company_id() ?: 1);
|
$companyId = (int) (company_id() ?: 1);
|
||||||
$document = CourierAgreementSignature::DOCUMENTS[$data['document_type']] ?? null;
|
$document = CourierAgreementSignature::DOCUMENTS[$data['document_type']] ?? null;
|
||||||
|
if ($data['document_type'] === 'cooperation-contract' && ($data['partner_type'] ?? 'courier') === 'supplier') {
|
||||||
|
$document = app(SupplierCooperationTemplates::class)->metadata((string) ($data['supplier_contract_template'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
if (! $document) {
|
if (! $document) {
|
||||||
throw ValidationException::withMessages(['document_type' => '不支持的协议文件类型。']);
|
throw ValidationException::withMessages(['document_type' => '不支持的协议文件类型。']);
|
||||||
|
|||||||
101
app/Support/SupplierCooperationTemplates.php
Normal file
101
app/Support/SupplierCooperationTemplates.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\Business\CourierAgreementSignature;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class SupplierCooperationTemplates
|
||||||
|
{
|
||||||
|
public const TEMPLATES = [
|
||||||
|
'dropshipping' => ['name' => '产品代发协议', 'code' => 'TPL-SUPPLIER-DROPSHIPPING-V1', 'version' => '1', 'file' => '产品代发协议-2022年确定版.docx', 'attachments' => 2],
|
||||||
|
'consignment' => ['name' => '产品代销协议', 'code' => 'TPL-SUPPLIER-CONSIGNMENT-V1', 'version' => '1', 'file' => '产品代销协议-2022确定版.docx', 'attachments' => 4],
|
||||||
|
'production' => ['name' => '生产合作协议', 'code' => 'TPL-SUPPLIER-PRODUCTION-V1', 'version' => '1', 'file' => '生产合作协议-2022确定版.docx', 'attachments' => 5],
|
||||||
|
];
|
||||||
|
|
||||||
|
public const FIELDS = [
|
||||||
|
'number' => ['label' => '合同编号', 'required' => true],
|
||||||
|
'location' => ['label' => '合同签订地', 'required' => true, 'default' => '常州市武进区'],
|
||||||
|
'party_a_address' => ['label' => '甲方地址'],
|
||||||
|
'party_a_legal' => ['label' => '甲方法定代表人'],
|
||||||
|
'party_a_contact' => ['label' => '甲方联系人'],
|
||||||
|
'party_a_email' => ['label' => '甲方邮箱', 'type' => 'email'],
|
||||||
|
'party_a_fax' => ['label' => '甲方传真'],
|
||||||
|
'party_b_address' => ['label' => '乙方地址'],
|
||||||
|
'party_b_legal' => ['label' => '乙方法定代表人'],
|
||||||
|
'party_b_contact' => ['label' => '乙方联系人'],
|
||||||
|
'party_b_email' => ['label' => '乙方邮箱', 'type' => 'email'],
|
||||||
|
'party_b_fax' => ['label' => '乙方传真'],
|
||||||
|
'account_name' => ['label' => '合同账户名称(原文为甲方付款账户)'],
|
||||||
|
'account_number' => ['label' => '合同银行账号'],
|
||||||
|
'bank_name' => ['label' => '合同开户行'],
|
||||||
|
'brands' => ['label' => '生产协议使用的品牌', 'default' => '亿家达、欧意朗', 'templates' => ['production']],
|
||||||
|
'product_model' => ['label' => '采购订单产品型号/SKU', 'templates' => ['consignment', 'production']],
|
||||||
|
'supply_price' => ['label' => '采购订单供货价格/明细说明', 'templates' => ['consignment', 'production']],
|
||||||
|
'purchase_contact' => ['label' => '采购对接人(订单附件)', 'templates' => ['consignment', 'production']],
|
||||||
|
'purchase_qq' => ['label' => '采购对接人 QQ', 'templates' => ['consignment', 'production']],
|
||||||
|
'purchase_wechat' => ['label' => '采购对接人微信', 'templates' => ['consignment', 'production']],
|
||||||
|
'purchase_phone' => ['label' => '采购对接人电话', 'templates' => ['consignment', 'production']],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function metadata(string $key): array
|
||||||
|
{
|
||||||
|
return self::TEMPLATES[$key] ?? throw ValidationException::withMessages([
|
||||||
|
'supplier_contract_template' => '请选择有效的供应商合作合同模板。',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function blocks(string $key): array
|
||||||
|
{
|
||||||
|
$this->metadata($key);
|
||||||
|
|
||||||
|
return json_decode(file_get_contents(resource_path("contracts/supplier/{$key}-v1.json")), true, 512, JSON_THROW_ON_ERROR)['blocks'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function values(array $payload): array
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
foreach (self::FIELDS as $key => $field) {
|
||||||
|
$values[$key] = trim((string) ($payload['supplier_contract_fields'][$key] ?? ''));
|
||||||
|
}
|
||||||
|
$values += [
|
||||||
|
'party_a' => $payload['cooperation_party_a_name'] ?? $payload['company_full_name'] ?? '',
|
||||||
|
'party_b' => $payload['cooperation_party_b_name'] ?? '',
|
||||||
|
'party_a_phone' => $payload['cooperation_party_a_phone'] ?? '',
|
||||||
|
'party_b_phone' => $payload['cooperation_party_b_phone'] ?? '',
|
||||||
|
'start_date' => $this->date($payload['cooperation_start_date'] ?? ''),
|
||||||
|
'end_date' => $this->date($payload['cooperation_end_date'] ?? ''),
|
||||||
|
'signature_date' => $this->date($payload['signature_date'] ?? ''),
|
||||||
|
'party_a_signer' => $payload['secondary_operator_name'] ?? '',
|
||||||
|
'party_b_signer' => $payload['supplier_contract_signer_name'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function text(string $text, array $values, bool $preview = false): string
|
||||||
|
{
|
||||||
|
$parts = preg_split('/(\[\[[a-z_]+\]\])/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
return implode('', array_map(static function (string $part) use ($values, $preview): string {
|
||||||
|
if (preg_match('/^\[\[([a-z_]+)\]\]$/', $part, $match)) {
|
||||||
|
$value = trim((string) ($values[$match[1]] ?? ''));
|
||||||
|
return $preview
|
||||||
|
? '<span class="agreement-preview-value" data-supplier-bind="' . e($match[1]) . '">' . e($value ?: '待填写') . '</span>'
|
||||||
|
: e($value ?: '________');
|
||||||
|
}
|
||||||
|
return e($part);
|
||||||
|
}, $parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSupplierContract(CourierAgreementSignature $signature): bool
|
||||||
|
{
|
||||||
|
return $signature->document_type === 'cooperation-contract'
|
||||||
|
&& ($signature->payload['partner_type'] ?? 'courier') === 'supplier';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function date(string $value): string
|
||||||
|
{
|
||||||
|
return $value === '' ? '' : Carbon::parse($value)->format('Y年n月j日');
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/img/supplier-contract-templates/production-1.png
Normal file
BIN
public/img/supplier-contract-templates/production-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
BIN
public/img/supplier-contract-templates/production-2.png
Normal file
BIN
public/img/supplier-contract-templates/production-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
public/img/supplier-contract-templates/production-3.png
Normal file
BIN
public/img/supplier-contract-templates/production-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
3330
resources/contracts/supplier/consignment-v1.json
Normal file
3330
resources/contracts/supplier/consignment-v1.json
Normal file
File diff suppressed because it is too large
Load Diff
2051
resources/contracts/supplier/dropshipping-v1.json
Normal file
2051
resources/contracts/supplier/dropshipping-v1.json
Normal file
File diff suppressed because it is too large
Load Diff
3795
resources/contracts/supplier/production-v1.json
Normal file
3795
resources/contracts/supplier/production-v1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@
|
|||||||
@php($agreementCourierNames = collect($agreementCourierNames ?? []))
|
@php($agreementCourierNames = collect($agreementCourierNames ?? []))
|
||||||
@php($agreementCourierAccounts = $agreementCourierAccounts ?? [])
|
@php($agreementCourierAccounts = $agreementCourierAccounts ?? [])
|
||||||
@php($agreementSupplierOptions = $agreementSupplierOptions ?? [])
|
@php($agreementSupplierOptions = $agreementSupplierOptions ?? [])
|
||||||
|
@php($supplierContractTemplates = \App\Support\SupplierCooperationTemplates::TEMPLATES)
|
||||||
|
@php($supplierContractRequestKey = (string) \Illuminate\Support\Str::uuid())
|
||||||
@php($agreementQuoteVersions = $agreementQuoteVersions ?? collect())
|
@php($agreementQuoteVersions = $agreementQuoteVersions ?? collect())
|
||||||
@php($agreementCooperationSecondaryEsignAccount = '18052733960')
|
@php($agreementCooperationSecondaryEsignAccount = '18052733960')
|
||||||
@php($agreementDefaultSubject = $agreementSubjects->firstWhere('is_default', true) ?: $agreementSubjects->first())
|
@php($agreementDefaultSubject = $agreementSubjects->firstWhere('is_default', true) ?: $agreementSubjects->first())
|
||||||
@ -50,6 +52,18 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.supplier-contract-body { line-height: 1.8; }
|
||||||
|
.supplier-contract-body .supplier-contract-heading { font-size: 14px; margin: 18px 0 8px; }
|
||||||
|
.supplier-contract-paragraph { margin: 0 0 8px; overflow-wrap: anywhere; }
|
||||||
|
.supplier-contract-table { width: 100%; border-collapse: collapse; table-layout: fixed; margin: 12px 0; }
|
||||||
|
.supplier-contract-table td { border: 1px solid var(--agreement-border); padding: 6px; vertical-align: top; }
|
||||||
|
.supplier-contract-signatures { width: 100%; table-layout: fixed; margin: 18px 0; }
|
||||||
|
.supplier-contract-signatures td { width: 50%; vertical-align: top; padding: 8px; }
|
||||||
|
.supplier-contract-signatures p { margin: 0 0 8px; }
|
||||||
|
.supplier-contract-seal-space { height: 85px; }
|
||||||
|
.supplier-contract-image { margin: 12px 0; text-align: center; }
|
||||||
|
.supplier-contract-image img { max-width: 100%; height: auto; }
|
||||||
|
|
||||||
.agreement-tabs,
|
.agreement-tabs,
|
||||||
.agreement-panel {
|
.agreement-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -1554,7 +1568,7 @@
|
|||||||
<div class="agreement-request-layout">
|
<div class="agreement-request-layout">
|
||||||
<form class="agreement-request-form" method="POST" action="{{ route('business.courier-agreement-signatures.store') }}" novalidate data-agreement-form data-signing-request-form>
|
<form class="agreement-request-form" method="POST" action="{{ route('business.courier-agreement-signatures.store') }}" novalidate data-agreement-form data-signing-request-form>
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="idempotency_key" value="{{ (string) \Illuminate\Support\Str::uuid() }}">
|
<input type="hidden" name="idempotency_key" value="{{ $supplierContractRequestKey }}">
|
||||||
<header class="agreement-request-header">
|
<header class="agreement-request-header">
|
||||||
<h2 class="agreement-request-title"><span class="material-icons-outlined" aria-hidden="true">draw</span><span>发起签章请求</span></h2>
|
<h2 class="agreement-request-title"><span class="material-icons-outlined" aria-hidden="true">draw</span><span>发起签章请求</span></h2>
|
||||||
<span class="agreement-status agreement-status--{{ $esignConfigured ? 'success' : 'warning' }}">{{ $esignConfigured ? 'E签宝可用' : '沙箱未配置' }}</span>
|
<span class="agreement-status agreement-status--{{ $esignConfigured ? 'success' : 'warning' }}">{{ $esignConfigured ? 'E签宝可用' : '沙箱未配置' }}</span>
|
||||||
@ -1676,7 +1690,7 @@
|
|||||||
<div class="agreement-field" data-secondary-signer-fields hidden>
|
<div class="agreement-field" data-secondary-signer-fields hidden>
|
||||||
<label for="agreement-secondary-esign-account"><span data-secondary-esign-label>第二签署方 E 签宝账号</span> <span class="agreement-required" aria-hidden="true">*</span></label>
|
<label for="agreement-secondary-esign-account"><span data-secondary-esign-label>第二签署方 E 签宝账号</span> <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||||
<input class="agreement-control" id="agreement-secondary-esign-account" name="secondary_esign_account" type="text" placeholder="填写另一签署方的手机号或邮箱" autocomplete="email" disabled>
|
<input class="agreement-control" id="agreement-secondary-esign-account" name="secondary_esign_account" type="text" placeholder="填写另一签署方的手机号或邮箱" autocomplete="email" disabled>
|
||||||
<p class="agreement-help">主体变更、合作合同和授权委托书需要双方分别实名签署;合作合同统一使用我方账号 {{ $agreementCooperationSecondaryEsignAccount }}。</p>
|
<p class="agreement-help">主体变更、合作合同和授权委托书需要双方分别实名签署;快递合作合同使用我方账号 {{ $agreementCooperationSecondaryEsignAccount }},供应商合作合同请填写我方签署账号。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="agreement-field" data-secondary-operator-field hidden>
|
<div class="agreement-field" data-secondary-operator-field hidden>
|
||||||
<label for="agreement-secondary-operator-name"><span data-secondary-operator-label>变更后主体 E 签宝实名认证姓名</span> <span class="agreement-required" aria-hidden="true">*</span></label>
|
<label for="agreement-secondary-operator-name"><span data-secondary-operator-label>变更后主体 E 签宝实名认证姓名</span> <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||||
@ -1820,8 +1834,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-template-fields="supplier-cooperation-pending" hidden>
|
<div data-supplier-contract-fields hidden>
|
||||||
<p class="agreement-help">供应商合作合同模板尚未提供。提供正式模板后,可在这里填写合同内容。</p>
|
<div class="agreement-field">
|
||||||
|
<label for="agreement-supplier-contract-template">合同模板 <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||||
|
<select class="agreement-control" id="agreement-supplier-contract-template" name="supplier_contract_template" data-supplier-contract-template disabled required>
|
||||||
|
@foreach ($supplierContractTemplates as $key => $template)
|
||||||
|
<option value="{{ $key }}" data-name="{{ $template['name'] }}" data-code="{{ $template['code'] }}" data-file="{{ $template['file'] }}">{{ $template['name'] }} · V{{ $template['version'] }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<p class="agreement-help">正文和附件按所选模板一起签署,签署任务保留本次模板版本。</p>
|
||||||
|
</div>
|
||||||
|
<div class="agreement-form-grid">
|
||||||
|
@foreach (\App\Support\SupplierCooperationTemplates::FIELDS as $key => $field)
|
||||||
|
<div class="agreement-field" data-supplier-field-templates="{{ implode(' ', $field['templates'] ?? array_keys($supplierContractTemplates)) }}">
|
||||||
|
<label for="supplier-contract-{{ $key }}">{{ $field['label'] }} @if($field['required'] ?? false)<span class="agreement-required" aria-hidden="true">*</span>@endif</label>
|
||||||
|
<input class="agreement-control" id="supplier-contract-{{ $key }}" name="supplier_contract_fields[{{ $key }}]" data-supplier-field="{{ $key }}" type="{{ $field['type'] ?? 'text' }}" value="{{ $key === 'number' ? 'SUP-' . strtoupper(substr(str_replace('-', '', $supplierContractRequestKey), 0, 12)) : ($field['default'] ?? '') }}" maxlength="{{ $key === 'supply_price' ? 1000 : 255 }}" @required($field['required'] ?? false) disabled>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<p class="agreement-help">按原合同填写银行账户资料;其余空白项可按本次合作情况补充,发送前请查看完整正文及附件。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-template-fields="authorization" hidden>
|
<div data-template-fields="authorization" hidden>
|
||||||
@ -2000,10 +2031,12 @@
|
|||||||
<p class="agreement-preview-page-number">原合同全文预览</p>
|
<p class="agreement-preview-page-number">原合同全文预览</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="agreement-preview-paper" data-template-preview="supplier-cooperation-pending" hidden>
|
<article class="agreement-preview-paper" data-template-preview="supplier-cooperation" hidden>
|
||||||
<h3>合作合同</h3>
|
@foreach ($supplierContractTemplates as $key => $template)
|
||||||
<p>供应商合作合同模板待提供。</p>
|
<div data-supplier-contract-preview="{{ $key }}" @if(!$loop->first) hidden @endif>
|
||||||
<p>提供正式合同模板后,可在这里查看合同预览。</p>
|
@include('business.partials.supplier-cooperation-contract', ['templateKey' => $key, 'mode' => 'preview', 'payload' => []])
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="agreement-preview-paper" data-template-preview="authorization" hidden>
|
<article class="agreement-preview-paper" data-template-preview="authorization" hidden>
|
||||||
@ -2092,6 +2125,9 @@
|
|||||||
<tr><td>公司主体变更</td><td>公司主体变更证明.docx</td><td>本地映射 V1</td><td>变更前后名称、变更日期、启用日期</td><td>变更前、变更后公司</td><td><span class="agreement-status agreement-status--warning">法人签字并盖章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
<tr><td>公司主体变更</td><td>公司主体变更证明.docx</td><td>本地映射 V1</td><td>变更前后名称、变更日期、启用日期</td><td>变更前、变更后公司</td><td><span class="agreement-status agreement-status--warning">法人签字并盖章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
||||||
<tr><td>共用合同证明</td><td>共用合同证明.docx</td><td>本地映射 V1</td><td>我方合作公司、物流商归属主体、共用合同快递、证明日期</td><td>物流商归属企业或个人</td><td><span class="agreement-status agreement-status--warning">企业盖章或个人签字</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
<tr><td>共用合同证明</td><td>共用合同证明.docx</td><td>本地映射 V1</td><td>我方合作公司、物流商归属主体、共用合同快递、证明日期</td><td>物流商归属企业或个人</td><td><span class="agreement-status agreement-status--warning">企业盖章或个人签字</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
||||||
<tr><td>合作合同</td><td>保丟不保损-合同发快递2024.8.doc</td><td>本地映射 V1</td><td>甲乙方名称、甲乙方电话、协议起止日期</td><td>甲方、乙方</td><td><span class="agreement-status agreement-status--warning">双方签字盖章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
<tr><td>合作合同</td><td>保丟不保损-合同发快递2024.8.doc</td><td>本地映射 V1</td><td>甲乙方名称、甲乙方电话、协议起止日期</td><td>甲方、乙方</td><td><span class="agreement-status agreement-status--warning">双方签字盖章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
||||||
|
@foreach ($supplierContractTemplates as $template)
|
||||||
|
<tr><td>{{ $template['name'] }}</td><td>{{ $template['file'] }}</td><td>V{{ $template['version'] }}</td><td>甲乙方资料、编号、日期、账户及附件变量</td><td>我方、供应商</td><td><span class="agreement-status agreement-status--warning">双方签署 · 含{{ $template['attachments'] }}项附件</span></td><td><span class="agreement-help">发起签章时选择</span></td></tr>
|
||||||
|
@endforeach
|
||||||
<tr><td>授权委托书</td><td>授权委托书(快递).docx</td><td>本地映射 V1</td><td>委托双方、授权事项、往来方、有效期</td><td>委托方、受托方</td><td><span class="agreement-status agreement-status--warning">双方签署</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
<tr><td>授权委托书</td><td>授权委托书(快递).docx</td><td>本地映射 V1</td><td>委托双方、授权事项、往来方、有效期</td><td>委托方、受托方</td><td><span class="agreement-status agreement-status--warning">双方签署</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
||||||
<tr><td>报价确认单</td><td>报价版本快照</td><td>本地映射 V1</td><td>版本号、快递公司、报价类型、有效期、报价明细</td><td>快递公司</td><td><span class="agreement-status agreement-status--warning">快递方签章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
<tr><td>报价确认单</td><td>报价版本快照</td><td>本地映射 V1</td><td>版本号、快递公司、报价类型、有效期、报价明细</td><td>快递公司</td><td><span class="agreement-status agreement-status--warning">快递方签章</span></td><td><button class="agreement-icon-button" type="button" disabled title="E签宝模板尚未创建"><span class="material-icons-outlined" aria-hidden="true">settings</span></button></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -2273,6 +2309,7 @@
|
|||||||
var supplierIdInput = root.querySelector('[data-signing-supplier-id]');
|
var supplierIdInput = root.querySelector('[data-signing-supplier-id]');
|
||||||
var supplierOptions = @json($agreementSupplierOptions);
|
var supplierOptions = @json($agreementSupplierOptions);
|
||||||
var documentSelect = form.querySelector('[data-document-type]');
|
var documentSelect = form.querySelector('[data-document-type]');
|
||||||
|
var supplierContractSelect = form.querySelector('[data-supplier-contract-template]');
|
||||||
var documentOptions = Array.prototype.slice.call(documentSelect.options);
|
var documentOptions = Array.prototype.slice.call(documentSelect.options);
|
||||||
var currentPartnerType = 'courier';
|
var currentPartnerType = 'courier';
|
||||||
var sendButton = root.querySelector('[data-sign-request-send]');
|
var sendButton = root.querySelector('[data-sign-request-send]');
|
||||||
@ -3171,7 +3208,7 @@
|
|||||||
|
|
||||||
function activateTemplate(templateType) {
|
function activateTemplate(templateType) {
|
||||||
root.querySelectorAll('[data-template-fields]').forEach(function (group) {
|
root.querySelectorAll('[data-template-fields]').forEach(function (group) {
|
||||||
var active = group.dataset.templateFields === templateType;
|
var active = group.dataset.templateFields === templateType || (templateType === 'supplier-cooperation' && group.dataset.templateFields === 'cooperation-contract');
|
||||||
group.hidden = !active;
|
group.hidden = !active;
|
||||||
group.querySelectorAll('input, select, textarea').forEach(function (control) {
|
group.querySelectorAll('input, select, textarea').forEach(function (control) {
|
||||||
control.disabled = !active;
|
control.disabled = !active;
|
||||||
@ -3310,16 +3347,16 @@
|
|||||||
function refreshPreview() {
|
function refreshPreview() {
|
||||||
var documentOption = selectedDocument();
|
var documentOption = selectedDocument();
|
||||||
var templateType = documentOption.value;
|
var templateType = documentOption.value;
|
||||||
var supplierCooperationPending = currentPartnerType === 'supplier' && templateType === 'cooperation-contract';
|
var supplierContract = currentPartnerType === 'supplier' && templateType === 'cooperation-contract';
|
||||||
var supplierUnavailable = currentPartnerType === 'supplier' && !supplierOptions.length;
|
var supplierUnavailable = currentPartnerType === 'supplier' && !supplierOptions.length;
|
||||||
sendButton.disabled = supplierCooperationPending || supplierUnavailable || !esignAvailable;
|
sendButton.disabled = supplierUnavailable || !esignAvailable;
|
||||||
sendButton.title = supplierCooperationPending ? '供应商合作合同模板尚未提供' : supplierUnavailable ? '供应商资料暂不可用' : (esignAvailable ? '推送至E签宝' : 'E签宝尚未配置');
|
sendButton.title = supplierUnavailable ? '供应商资料暂不可用' : (esignAvailable ? '推送至E签宝' : 'E签宝尚未配置');
|
||||||
sendButton.querySelector('span:last-child').textContent = supplierCooperationPending ? '合作合同模板待提供' : '推送至E签宝';
|
sendButton.querySelector('span:last-child').textContent = '推送至E签宝';
|
||||||
var courier = fieldValue('[name="courier_name"]', currentPartnerType === 'supplier' ? '供应商' : '快递公司');
|
var courier = fieldValue('[name="courier_name"]', currentPartnerType === 'supplier' ? '供应商' : '快递公司');
|
||||||
var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称');
|
var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称');
|
||||||
var accountAgreement = templateType === 'account-proof' || templateType === 'account-change';
|
var accountAgreement = templateType === 'account-proof' || templateType === 'account-change';
|
||||||
var entityChange = templateType === 'entity-change';
|
var entityChange = templateType === 'entity-change';
|
||||||
var cooperationContract = templateType === 'cooperation-contract' && !supplierCooperationPending;
|
var cooperationContract = templateType === 'cooperation-contract';
|
||||||
var authorization = templateType === 'authorization';
|
var authorization = templateType === 'authorization';
|
||||||
var sharedContract = templateType === 'shared-contract';
|
var sharedContract = templateType === 'shared-contract';
|
||||||
var formerCompanyDeregistered = entityChange && entityChangeDeregisteredInput && entityChangeDeregisteredInput.checked;
|
var formerCompanyDeregistered = entityChange && entityChangeDeregisteredInput && entityChangeDeregisteredInput.checked;
|
||||||
@ -3386,7 +3423,17 @@
|
|||||||
? '填写与 E 签宝账号实名一致的个人姓名;该姓名会作为个人签署人发送,不需要填写企业全称。'
|
? '填写与 E 签宝账号实名一致的个人姓名;该姓名会作为个人签署人发送,不需要填写企业全称。'
|
||||||
: '必须与对方 E 签宝企业认证名称一致;这是签署主体,不要求与银行卡持卡人姓名一致。';
|
: '必须与对方 E 签宝企业认证名称一致;这是签署主体,不要求与银行卡持卡人姓名一致。';
|
||||||
}
|
}
|
||||||
activateTemplate(supplierCooperationPending ? 'supplier-cooperation-pending' : templateType);
|
activateTemplate(supplierContract ? 'supplier-cooperation' : templateType);
|
||||||
|
root.querySelector('[data-supplier-contract-fields]').hidden = !supplierContract;
|
||||||
|
supplierContractSelect.disabled = !supplierContract;
|
||||||
|
root.querySelectorAll('[data-supplier-field-templates]').forEach(function (group) {
|
||||||
|
var active = supplierContract && group.dataset.supplierFieldTemplates.split(' ').indexOf(supplierContractSelect.value) !== -1;
|
||||||
|
group.hidden = !active;
|
||||||
|
group.querySelectorAll('input').forEach(function (input) { input.disabled = !active; });
|
||||||
|
});
|
||||||
|
root.querySelectorAll('[data-supplier-contract-preview]').forEach(function (preview) {
|
||||||
|
preview.hidden = preview.dataset.supplierContractPreview !== supplierContractSelect.value;
|
||||||
|
});
|
||||||
setText('[data-cooperation-party-b-label]', otherSignerIsPerson ? '乙方个人姓名(E 签宝实名)' : '乙方名称');
|
setText('[data-cooperation-party-b-label]', otherSignerIsPerson ? '乙方个人姓名(E 签宝实名)' : '乙方名称');
|
||||||
root.querySelector('[name="cooperation_party_b_name"]').placeholder = otherSignerIsPerson
|
root.querySelector('[name="cooperation_party_b_name"]').placeholder = otherSignerIsPerson
|
||||||
? '填写合作人的 E 签宝实名认证姓名'
|
? '填写合作人的 E 签宝实名认证姓名'
|
||||||
@ -3402,7 +3449,7 @@
|
|||||||
control.disabled = !needsSecondary;
|
control.disabled = !needsSecondary;
|
||||||
control.required = needsSecondary;
|
control.required = needsSecondary;
|
||||||
|
|
||||||
if (templateType === 'cooperation-contract') {
|
if (templateType === 'cooperation-contract' && currentPartnerType === 'courier') {
|
||||||
control.value = cooperationSecondaryEsignAccount;
|
control.value = cooperationSecondaryEsignAccount;
|
||||||
control.readOnly = true;
|
control.readOnly = true;
|
||||||
} else {
|
} else {
|
||||||
@ -3414,7 +3461,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (templateType === 'cooperation-contract' && secondaryEsignAccountInput) {
|
if (templateType === 'cooperation-contract' && currentPartnerType === 'courier' && secondaryEsignAccountInput) {
|
||||||
secondaryEsignAccountInput.dataset.cooperationDefault = 'true';
|
secondaryEsignAccountInput.dataset.cooperationDefault = 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3425,13 +3472,15 @@
|
|||||||
secondaryOperatorInput.required = needsSecondaryOperator;
|
secondaryOperatorInput.required = needsSecondaryOperator;
|
||||||
setText('[data-secondary-operator-label]', entityChange ? '变更后主体 E 签宝实名认证姓名' : cooperationContract ? '我方 E 签宝实名认证姓名' : '受托方 E 签宝实名认证姓名');
|
setText('[data-secondary-operator-label]', entityChange ? '变更后主体 E 签宝实名认证姓名' : cooperationContract ? '我方 E 签宝实名认证姓名' : '受托方 E 签宝实名认证姓名');
|
||||||
}
|
}
|
||||||
setText('[data-template-name]', documentOption.dataset.name);
|
var contractOption = supplierContractSelect.options[supplierContractSelect.selectedIndex];
|
||||||
setText('[data-template-code]', supplierCooperationPending ? '模板待提供' : documentOption.dataset.code);
|
var selectedMetadata = supplierContract ? contractOption.dataset : documentOption.dataset;
|
||||||
setText('[data-template-description]', supplierCooperationPending ? '供应商合作合同模板尚未提供。提供正式模板后,可填写并预览。' : formerCompanyDeregistered ? '原法人以个人身份签字,变更后公司仍须法人签字并盖章;双方分别接收签署请求。' : otherSignerIsPerson ? '使用合作人的个人实名认证姓名和账号签署。' : documentOption.dataset.description);
|
setText('[data-template-name]', selectedMetadata.name);
|
||||||
setText('[data-variable-template-name]', documentOption.dataset.name);
|
setText('[data-template-code]', selectedMetadata.code);
|
||||||
setText('[data-variable-template-status]', supplierCooperationPending ? '— 模板待提供' : '— 填写变量');
|
setText('[data-template-description]', supplierContract ? '原文件:' + selectedMetadata.file + ';正文和附件由双方签署并回传。' : formerCompanyDeregistered ? '原法人以个人身份签字,变更后公司仍须法人签字并盖章;双方分别接收签署请求。' : otherSignerIsPerson ? '使用合作人的个人实名认证姓名和账号签署。' : documentOption.dataset.description);
|
||||||
setText('[data-preview-template-code]', supplierCooperationPending ? '模板待提供' : documentOption.dataset.code);
|
setText('[data-variable-template-name]', selectedMetadata.name);
|
||||||
setText('[data-preview-status-hint]', supplierCooperationPending ? '提供模板后可预览' : '蓝色=已填 红色=待填');
|
setText('[data-variable-template-status]', '— 填写变量');
|
||||||
|
setText('[data-preview-template-code]', selectedMetadata.code);
|
||||||
|
setText('[data-preview-status-hint]', '已填写变量随表单更新');
|
||||||
setPreviewValue('company', companyName);
|
setPreviewValue('company', companyName);
|
||||||
setPreviewValue('courier', courier);
|
setPreviewValue('courier', courier);
|
||||||
setPreviewValue('signatureDate', formatDateField('[name="signature_date"]', '待填写'));
|
setPreviewValue('signatureDate', formatDateField('[name="signature_date"]', '待填写'));
|
||||||
@ -3502,6 +3551,25 @@
|
|||||||
setPreviewValue('cooperationPartyBPhone', fieldValue('[name="cooperation_party_b_phone"]', '待填写'));
|
setPreviewValue('cooperationPartyBPhone', fieldValue('[name="cooperation_party_b_phone"]', '待填写'));
|
||||||
setPreviewValue('cooperationStartDate', formatDateField('[name="cooperation_start_date"]', '待填写'));
|
setPreviewValue('cooperationStartDate', formatDateField('[name="cooperation_start_date"]', '待填写'));
|
||||||
setPreviewValue('cooperationEndDate', formatDateField('[name="cooperation_end_date"]', '待填写'));
|
setPreviewValue('cooperationEndDate', formatDateField('[name="cooperation_end_date"]', '待填写'));
|
||||||
|
if (supplierContract) {
|
||||||
|
var supplierValues = {
|
||||||
|
party_a: fieldValue('[name="cooperation_party_a_name"]', companyName),
|
||||||
|
party_b: fieldValue('[name="cooperation_party_b_name"]', ''),
|
||||||
|
party_a_phone: fieldValue('[name="cooperation_party_a_phone"]', ''),
|
||||||
|
party_b_phone: fieldValue('[name="cooperation_party_b_phone"]', ''),
|
||||||
|
party_a_signer: fieldValue('[name="secondary_operator_name"]', ''),
|
||||||
|
party_b_signer: otherSignerIsPerson ? fieldValue('[name="cooperation_party_b_name"]', '') : fieldValue('[name="operator_name"]', ''),
|
||||||
|
signature_date: formatDateField('[name="signature_date"]', ''),
|
||||||
|
start_date: formatDateField('[name="cooperation_start_date"]', ''),
|
||||||
|
end_date: formatDateField('[name="cooperation_end_date"]', '')
|
||||||
|
};
|
||||||
|
root.querySelectorAll('[data-supplier-field]').forEach(function (input) { supplierValues[input.dataset.supplierField] = input.value.trim(); });
|
||||||
|
root.querySelectorAll('[data-supplier-bind]').forEach(function (element) {
|
||||||
|
var value = supplierValues[element.dataset.supplierBind] || '';
|
||||||
|
element.textContent = value || '待填写';
|
||||||
|
element.classList.toggle('is-empty', !value);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateType === 'authorization') {
|
if (templateType === 'authorization') {
|
||||||
@ -3806,11 +3874,6 @@
|
|||||||
|
|
||||||
form.addEventListener('submit', function (event) {
|
form.addEventListener('submit', function (event) {
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
if (currentPartnerType === 'supplier' && selectedDocument().value === 'cooperation-contract') {
|
|
||||||
event.preventDefault();
|
|
||||||
root.querySelector('[data-request-error]').textContent = '供应商合作合同模板尚未提供,暂不能发起签署。';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var error = root.querySelector('[data-request-error]');
|
var error = root.querySelector('[data-request-error]');
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
@if (($block['text'] ?? '') !== '')
|
||||||
|
@if (($block['heading'] ?? false))
|
||||||
|
<h2 class="supplier-contract-heading" @if(($block['align'] ?? '') === 'center') style="text-align:center" @endif>{!! $supplierTemplates->text($block['text'], $supplierValues, $supplierPreview) !!}</h2>
|
||||||
|
@else
|
||||||
|
<p class="supplier-contract-paragraph @if($block['signature'] ?? false) supplier-contract-signature @endif" @if(($block['align'] ?? '') === 'center') style="text-align:center" @endif>{!! $supplierTemplates->text($block['text'], $supplierValues, $supplierPreview) !!}</p>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
@foreach ($block['images'] ?? [] as $image)
|
||||||
|
<div class="supplier-contract-image"><img src="{{ $supplierPreview ? asset('public/img/supplier-contract-templates/' . $image) : 'data:image/png;base64,' . base64_encode(file_get_contents(public_path('img/supplier-contract-templates/' . $image))) }}" alt="产品包装与合格证示意图"></div>
|
||||||
|
@endforeach
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
@php
|
||||||
|
$supplierTemplates = app(\App\Support\SupplierCooperationTemplates::class);
|
||||||
|
$supplierValues = $supplierTemplates->values($payload ?? []);
|
||||||
|
$supplierPreview = ($mode ?? 'pdf') === 'preview';
|
||||||
|
@endphp
|
||||||
|
<div class="supplier-contract-body">
|
||||||
|
@foreach ($supplierTemplates->blocks($templateKey) as $block)
|
||||||
|
@if ($block['type'] === 'signatures')
|
||||||
|
<table class="supplier-contract-signatures">
|
||||||
|
<tr>
|
||||||
|
@foreach (['a' => '甲方', 'b' => '乙方'] as $party => $label)
|
||||||
|
<td>
|
||||||
|
<p>{{ $label }}:{!! $supplierTemplates->text('[[party_' . $party . ']]', $supplierValues, $supplierPreview) !!}</p>
|
||||||
|
<p>授权代表:{!! $supplierTemplates->text('[[party_' . $party . '_signer]]', $supplierValues, $supplierPreview) !!}</p>
|
||||||
|
<p>签字/盖章:</p>
|
||||||
|
<div class="supplier-contract-seal-space"></div>
|
||||||
|
<p>合同签订日期:{!! $supplierTemplates->text('[[signature_date]]', $supplierValues, $supplierPreview) !!}</p>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
@elseif ($block['type'] === 'table')
|
||||||
|
<table class="supplier-contract-table">
|
||||||
|
<tbody>
|
||||||
|
@foreach ($block['rows'] as $row)
|
||||||
|
<tr>
|
||||||
|
@foreach ($row as $cell)
|
||||||
|
<td style="width:{{ $cell['width'] }}%" @if(($cell['colspan'] ?? 1) > 1) colspan="{{ $cell['colspan'] }}" @endif @if(($cell['rowspan'] ?? 1) > 1) rowspan="{{ $cell['rowspan'] }}" @endif>
|
||||||
|
@foreach ($cell['paragraphs'] as $paragraph)
|
||||||
|
@include('business.partials.supplier-contract-paragraph', ['block' => $paragraph])
|
||||||
|
@endforeach
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@else
|
||||||
|
@include('business.partials.supplier-contract-paragraph')
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
@font-face { font-family: contract-fallback; font-style: normal; font-weight: 400; src: url("file://{{ $fontPath }}") format("truetype"); }
|
||||||
|
@font-face { font-family: contract-fallback; font-style: normal; font-weight: 700; src: url("file://{{ $fontPath }}") format("truetype"); }
|
||||||
|
@page { margin: 20mm 17mm; }
|
||||||
|
body { font-family: contract-fallback, sans-serif; color: #171717; font-size: 12px; line-height: 1.65; }
|
||||||
|
.latin { font-family: "DejaVu Sans", sans-serif; }
|
||||||
|
.supplier-contract-heading { font-size: 15px; margin: 16px 0 8px; page-break-after: avoid; }
|
||||||
|
.supplier-contract-paragraph { margin: 0 0 7px; overflow-wrap: anywhere; }
|
||||||
|
.supplier-contract-signature { margin-top: 18px; min-height: 40px; page-break-inside: avoid; }
|
||||||
|
.supplier-contract-table { width: 100%; border-collapse: collapse; table-layout: fixed; margin: 10px 0; page-break-inside: avoid; }
|
||||||
|
.supplier-contract-table td { border: 1px solid #999; padding: 6px; vertical-align: top; }
|
||||||
|
.supplier-contract-table p { margin-bottom: 4px; }
|
||||||
|
.supplier-contract-image { text-align: center; margin: 12px 0; page-break-inside: avoid; }
|
||||||
|
.supplier-contract-image img { max-width: 100%; max-height: 430px; }
|
||||||
|
.supplier-contract-signatures { width: 100%; table-layout: fixed; page-break-inside: avoid; margin: 18px 0; }
|
||||||
|
.supplier-contract-signatures td { width: 50%; vertical-align: top; padding: 8px; }
|
||||||
|
.supplier-contract-signatures p { margin: 0 0 8px; }
|
||||||
|
.supplier-contract-seal-space { height: 85px; }
|
||||||
|
.meta { color: #595959; font-size: 9px; text-align: right; margin-bottom: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="meta">业务单号:{{ $signature->business_no }} 模板:{{ $signature->template_code }} 版本:{{ $signature->template_version }}</div>
|
||||||
|
@include('business.partials.supplier-cooperation-contract', ['templateKey' => $payload['supplier_contract_template'], 'mode' => 'pdf'])
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -98,7 +98,7 @@ class AgreementSigningPageTest extends FeatureTestCase
|
|||||||
->assertSeeText('仅用于 E 签宝身份核验,不是收款人姓名')
|
->assertSeeText('仅用于 E 签宝身份核验,不是收款人姓名')
|
||||||
->assertSee("setPreviewValue('proofSigner', '公司盖章')", false)
|
->assertSee("setPreviewValue('proofSigner', '公司盖章')", false)
|
||||||
->assertSee("setPreviewValue('changeSigner', '公司盖章')", false)
|
->assertSee("setPreviewValue('changeSigner', '公司盖章')", false)
|
||||||
->assertSeeText('合作合同统一使用我方账号 18052733960')
|
->assertSeeText('快递合作合同使用我方账号 18052733960')
|
||||||
->assertSeeText('我方 E 签宝实名认证姓名')
|
->assertSeeText('我方 E 签宝实名认证姓名')
|
||||||
->assertSee('name="secondary_operator_name"', false)
|
->assertSee('name="secondary_operator_name"', false)
|
||||||
->assertSeeText('填写变量')
|
->assertSeeText('填写变量')
|
||||||
@ -124,6 +124,24 @@ class AgreementSigningPageTest extends FeatureTestCase
|
|||||||
$this->assertSame('张经理', $response->viewData('agreementSupplierOptions')[0]['operator']);
|
$this->assertSame('张经理', $response->viewData('agreementSupplierOptions')[0]['operator']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_supplier_contract_templates_include_full_preview_and_editable_variables(): void
|
||||||
|
{
|
||||||
|
$this->grantWorkspaceAccess();
|
||||||
|
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||||
|
'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'create',
|
||||||
|
]))->assertOk();
|
||||||
|
foreach (['dropshipping', 'consignment', 'production'] as $template) {
|
||||||
|
$response->assertSee('data-supplier-contract-preview="' . $template . '"', false);
|
||||||
|
$response->assertSeeText(\App\Support\SupplierCooperationTemplates::TEMPLATES[$template]['name']);
|
||||||
|
$response->assertSee(\App\Support\SupplierCooperationTemplates::TEMPLATES[$template]['code'], false);
|
||||||
|
}
|
||||||
|
$response->assertSee('name="supplier_contract_template"', false)
|
||||||
|
->assertSee('name="supplier_contract_fields[number]"', false)
|
||||||
|
->assertSee('data-supplier-bind="party_a"', false)
|
||||||
|
->assertSee('production-3.png', false)
|
||||||
|
->assertDontSeeText('供应商合作合同模板尚未提供');
|
||||||
|
}
|
||||||
|
|
||||||
public function testAgreementSigningPageUsesCourierNamesFromCourierInformation(): void
|
public function testAgreementSigningPageUsesCourierNamesFromCourierInformation(): void
|
||||||
{
|
{
|
||||||
$this->grantWorkspaceAccess();
|
$this->grantWorkspaceAccess();
|
||||||
|
|||||||
@ -9,6 +9,7 @@ use App\Models\Business\CourierQuoteChange;
|
|||||||
use App\Models\Business\CourierQuoteSignature;
|
use App\Models\Business\CourierQuoteSignature;
|
||||||
use App\Models\Business\CourierQuoteVersion;
|
use App\Models\Business\CourierQuoteVersion;
|
||||||
use App\Support\CourierAgreementPdf;
|
use App\Support\CourierAgreementPdf;
|
||||||
|
use App\Support\SupplierCooperationTemplates;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
use Illuminate\Http\Client\Request as ClientRequest;
|
use Illuminate\Http\Client\Request as ClientRequest;
|
||||||
@ -136,11 +137,97 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
|||||||
$payload['partner_type'] = 'supplier';
|
$payload['partner_type'] = 'supplier';
|
||||||
$payload['supplier_id'] = 7;
|
$payload['supplier_id'] = 7;
|
||||||
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
|
||||||
->assertSessionHasErrors(['document_type' => '供应商合作合同模板尚未提供,暂不能发起签署。']);
|
->assertSessionHasErrors('supplier_contract_template');
|
||||||
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
||||||
Http::assertNothingSent();
|
Http::assertNothingSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @dataProvider supplierTemplateProvider */
|
||||||
|
public function test_supplier_contract_keeps_the_selected_template_and_archives_the_signed_file(string $template): void
|
||||||
|
{
|
||||||
|
$this->fakeEsign();
|
||||||
|
$payload = $this->supplierContractPayload($template);
|
||||||
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
||||||
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
||||||
|
$metadata = SupplierCooperationTemplates::TEMPLATES[$template];
|
||||||
|
$this->assertSame($metadata['code'], $signature->template_code);
|
||||||
|
$this->assertSame('1', $signature->template_version);
|
||||||
|
$this->assertSame($metadata['file'], $signature->source_file_name);
|
||||||
|
$this->assertSame($metadata['name'], $signature->documentLabel());
|
||||||
|
$this->assertSame($template, $signature->payload['supplier_contract_template']);
|
||||||
|
$this->assertSame('SUP-TEST-001', $signature->payload['supplier_contract_fields']['number']);
|
||||||
|
$this->assertSame('张经理', $signature->payload['supplier_contract_signer_name']);
|
||||||
|
$source = Storage::disk('uploads')->get($signature->source_pdf_path);
|
||||||
|
$this->assertStringStartsWith('%PDF-', $source);
|
||||||
|
$this->assertSame(hash('sha256', $source), $signature->source_pdf_sha256);
|
||||||
|
Http::assertSent(function (ClientRequest $request): bool {
|
||||||
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') return false;
|
||||||
|
$signers = $request->data()['signers'] ?? [];
|
||||||
|
return count($signers) === 2
|
||||||
|
&& data_get($signers, '0.orgSignerInfo.orgName') === $this->company->name
|
||||||
|
&& data_get($signers, '0.orgSignerInfo.transactorInfo.psnAccount') === '13900139000'
|
||||||
|
&& data_get($signers, '0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '李经理'
|
||||||
|
&& data_get($signers, '1.orgSignerInfo.orgName') === '测试供应商有限公司'
|
||||||
|
&& data_get($signers, '1.orgSignerInfo.transactorInfo.psnAccount') === '13800138000'
|
||||||
|
&& data_get($signers, '1.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理';
|
||||||
|
});
|
||||||
|
Http::swap(new \Illuminate\Http\Client\Factory());
|
||||||
|
Http::fake([
|
||||||
|
'https://smlopenapi.esign.test/v3/sign-flow/flow-agreement-001/detail' => Http::response(['code' => 0, 'data' => ['signFlowStatus' => 2]]),
|
||||||
|
'https://smlopenapi.esign.test/v3/sign-flow/flow-agreement-001/file-download-url' => Http::response(['code' => 0, 'data' => ['files' => [['downloadUrl' => 'https://download.esign.test/supplier-signed']]]]),
|
||||||
|
'https://download.esign.test/supplier-signed' => Http::response('%PDF-supplier-signed'),
|
||||||
|
]);
|
||||||
|
$archived = app(\App\Support\CourierAgreementSignatureService::class)->synchronize($signature, $this->user->id);
|
||||||
|
$this->assertSame(CourierAgreementSignature::STATUS_ARCHIVED, $archived->status);
|
||||||
|
$this->assertSame('%PDF-supplier-signed', Storage::disk('uploads')->get($archived->signed_pdf_path));
|
||||||
|
$this->assertSame($metadata['code'], $archived->template_code);
|
||||||
|
$this->assertSame($source, Storage::disk('uploads')->get($archived->source_pdf_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function supplierTemplateProvider(): array
|
||||||
|
{
|
||||||
|
return [['dropshipping'], ['consignment'], ['production']];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_supplier_contract_rejects_invalid_template_and_missing_contract_identity(): void
|
||||||
|
{
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
$this->withExceptionHandling();
|
||||||
|
$payload = $this->cooperationContractPayload('invalid-supplier-contract');
|
||||||
|
$payload += ['partner_type' => 'supplier', 'supplier_id' => 7, 'supplier_contract_template' => '../other'];
|
||||||
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertSessionHasErrors('supplier_contract_template');
|
||||||
|
$payload = $this->supplierContractPayload('dropshipping');
|
||||||
|
$payload['supplier_contract_fields']['number'] = '';
|
||||||
|
$payload['supplier_contract_fields']['location'] = '';
|
||||||
|
$this->post(route('business.courier-agreement-signatures.store'), $payload)->assertSessionHasErrors(['supplier_contract_fields.number', 'supplier_contract_fields.location']);
|
||||||
|
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
||||||
|
Http::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_supplier_contract_variables_are_escaped_and_cover_the_body_and_appendices(): void
|
||||||
|
{
|
||||||
|
$templates = app(SupplierCooperationTemplates::class);
|
||||||
|
foreach (array_keys(SupplierCooperationTemplates::TEMPLATES) as $template) {
|
||||||
|
$html = view('business.partials.supplier-cooperation-contract', [
|
||||||
|
'templateKey' => $template, 'mode' => 'preview',
|
||||||
|
'payload' => ['cooperation_party_a_name' => '<script>alert(1)</script>', 'cooperation_party_b_name' => '新供应商', 'supplier_contract_fields' => ['number' => 'SUP-NEW-001']],
|
||||||
|
])->render();
|
||||||
|
$this->assertStringContainsString('<script>', $html);
|
||||||
|
$this->assertStringNotContainsString('<script>', $html);
|
||||||
|
$this->assertStringContainsString('SUP-NEW-001', $html);
|
||||||
|
$this->assertStringContainsString('新供应商', $html);
|
||||||
|
$this->assertStringNotContainsString('[[', $html);
|
||||||
|
$this->assertStringNotContainsString('沭阳简华家具有限公司', $html);
|
||||||
|
$this->assertStringNotContainsString('常州市武进飞燕被服有限公司', $html);
|
||||||
|
$blocks = $templates->blocks($template);
|
||||||
|
$text = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
|
for ($attachment = 1; $attachment <= SupplierCooperationTemplates::TEMPLATES[$template]['attachments']; $attachment++) {
|
||||||
|
$this->assertStringContainsString('附件' . $attachment, $text);
|
||||||
|
}
|
||||||
|
$this->assertSame(2, count(array_filter($blocks, fn ($block) => $block['type'] === 'table')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function test_unknown_partner_type_is_rejected_before_sending(): void
|
public function test_unknown_partner_type_is_rejected_before_sending(): void
|
||||||
{
|
{
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
@ -1359,6 +1446,20 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function supplierContractPayload(string $template): array
|
||||||
|
{
|
||||||
|
$repository = Mockery::mock(SupplierManagementMirrorRepository::class);
|
||||||
|
$repository->shouldReceive('signingSupplier')->with(7)->once()->andReturn((object) [
|
||||||
|
'Id' => 7, 'scmname' => '测试供应商', 'accountbankno' => '13800138000',
|
||||||
|
]);
|
||||||
|
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
|
||||||
|
return array_merge($this->cooperationContractPayload('supplier-' . $template), [
|
||||||
|
'partner_type' => 'supplier', 'supplier_id' => 7, 'supplier_contract_template' => $template,
|
||||||
|
'cooperation_party_b_name' => '测试供应商有限公司',
|
||||||
|
'supplier_contract_fields' => ['number' => 'SUP-TEST-001', 'location' => '测试签订地', 'party_b_address' => '测试供应商地址'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function accountProofPayload(string $idempotencyKey): array
|
private function accountProofPayload(string $idempotencyKey): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user