export const CRM_INVOICE_PASTE_SAMPLE_ANQING = `开票资料: 公司名称:安庆市山峡化工有限公司 纳税人识别号:9134080006084212XH 开户行:中国建设银行安庆分行城中支行 帐号:34050168860800000121 银行行号:105368000056 地址:安徽省安庆市高新区霞虹路5号 电话/传真:0556-5348918 邮编:246000 发票邮寄地址: 姓名:汪璇 电话:18895395317 地址:安徽省安庆市高新区霞虹路5号` export const CRM_INVOICE_PASTE_SAMPLE_JIANGFENG = `单位名:安徽江峰生物科技有限公司 税 号:91340200MA8PYJNB8X 地址及电话:安徽省芜湖市皖江江北新兴产业集中区沈巷片区起步区皖跃路与和睦路交叉口西南角福州路4号 银行开户行:中国建设银行芜湖经开区支行 账 号:3405 0167 8808 0000 2481 银行行号:1053 6205 0066 电话:0553-5201019` export interface CrmInvoicePasteResult { companyName?: string taxNo?: string companyAddress?: string legalRepresentative?: string agent?: string phoneFax?: string bankName?: string bankAccount?: string remarkParts: string[] warnings: string[] filledCount: number } function normalizeSpaces(s: string): string { return (s || '').replace(/\s+/g, ' ').trim() } function splitMailBlock(text: string): { main: string; mail: string } { const idx = text.search(/发票邮寄地址\s*[::]?/) if (idx < 0) return { main: text, mail: '' } return { main: text.slice(0, idx), mail: text.slice(idx).replace(/^发票邮寄地址\s*[::]?/, '') } } function pickLine(block: string, labelRe: RegExp): string { const lines = block.split(/\r?\n/) for (const raw of lines) { const line = raw.trim() if (!line) continue const m = line.match(labelRe) if (m && m[1] != null) return normalizeSpaces(m[1]) } return '' } const PHONE_TAIL = /((?:0\d{2,3}-?\d{7,8})|(?:1\d{10}))\s*$/ export function parseCrmInvoicePaste(raw: string): CrmInvoicePasteResult { const warnings: string[] = [] const text = (raw || '').replace(/\r\n/g, '\n').trim() if (!text) { return { remarkParts: [], warnings: ['请先粘贴开票资料文本'], filledCount: 0 } } const { main, mail } = splitMailBlock(text) const remarkParts: string[] = [] const companyName = pickLine( main, /^(?:公司名称|单位名称|单位名|名称)\s*[::]\s*(.+)$/ ) const taxNo = pickLine( main, /^(?:纳税人识别号|纳税识别号|统一社会信用代码|税\s*号|税号)\s*[::]\s*([0-9A-Za-z]+)\s*$/ ) const bankName = pickLine( main, /^(?:银行开户行|开户银行|开户行)\s*[::]\s*(.+)$/ ) let bankAccount = pickLine( main, /^(?:银行账号|账\s*号|帐\s*号|账号|帐号)\s*[::]\s*([0-9\s]+)\s*$/ ) if (bankAccount) bankAccount = bankAccount.replace(/\s+/g, '') let companyAddress = '' let phoneFax = '' const addrPhone = pickLine(main, /^(?:地址及电话)\s*[::]\s*(.+)$/) if (addrPhone) { const m = addrPhone.match(PHONE_TAIL) if (m && m.index != null) { phoneFax = m[1] companyAddress = normalizeSpaces(addrPhone.slice(0, m.index)) } else { companyAddress = addrPhone } } else { companyAddress = pickLine(main, /^(?:单位地址|地\s*址|地址)\s*[::]\s*(.+)$/) } if (!phoneFax) { phoneFax = pickLine(main, /^(?:电话\/传真|电话传真|传真)\s*[::]\s*(.+)$/) || pickLine(main, /^(?:电\s*话|电话)\s*[::]\s*(.+)$/) } const legalRepresentative = pickLine( main, /^(?:法定代表人|法人代表|法人)\s*[::]\s*(.+)$/ ) const agent = pickLine(main, /^(?:委托代理人|代理人)\s*[::]\s*(.+)$/) const bankCode = pickLine(main, /^(?:银行行号|联行号|行号)\s*[::]\s*([0-9\s]+)\s*$/) if (bankCode) remarkParts.push(`银行行号:${bankCode.replace(/\s+/g, '')}`) const zip = pickLine(main, /^(?:邮\s*编|邮编)\s*[::]\s*(\d{6})\s*$/) if (zip) remarkParts.push(`邮编:${zip}`) if (mail.trim()) { const name = pickLine(mail, /^(?:姓\s*名|姓名)\s*[::]\s*(.+)$/) const phone = pickLine(mail, /^(?:电\s*话|电话|手机)\s*[::]\s*(.+)$/) const addr = pickLine(mail, /^(?:地\s*址|地址)\s*[::]\s*(.+)$/) const bits = [name && `收件人 ${name}`, phone, addr].filter(Boolean) as string[] if (bits.length) remarkParts.push(`邮寄:${bits.join(' / ')}`) } const fields = { companyName: companyName || undefined, taxNo: taxNo || undefined, companyAddress: companyAddress || undefined, legalRepresentative: legalRepresentative || undefined, agent: agent || undefined, phoneFax: phoneFax || undefined, bankName: bankName || undefined, bankAccount: bankAccount || undefined } const filledCount = Object.values(fields).filter(Boolean).length if (!companyName) warnings.push('未识别到单位名称') if (!taxNo) warnings.push('未识别到税号') if (filledCount === 0) warnings.push('未能识别任何开票字段,请检查格式或手动填写') return { ...fields, remarkParts, warnings, filledCount } } export function mergeInvoiceRemark(existing: string | undefined, parts: string[]): string { const base = (existing || '').trim() const extras = (parts || []).map((p) => p.trim()).filter(Boolean) if (!extras.length) return base let next = base for (const p of extras) { if (!next) { next = p continue } if (next.includes(p)) continue next = `${next};${p}` } return next }