土耳其里拉结算的 AdSense 预扣税,我用 Apps Script 对了一次账
This post is available in 简体中文 only — the original follows.
上个月帮伊斯坦布尔一个小工作室理账,他们的 AdSense 后台是土耳其区账号,结算币种 TRY,付款资料里填的是本地税号。问题出在「预扣税」那一栏:后台显示的是税前收入和实际到账,中间少了一截,但每月数字对不上,工作室的会计坚持说按 15% 扣的,后台看下来有的月份接近 17%,有的月份又像 10%。他们把过去 14 个月的付款邮件和后台 CSV 都导了出来,手工核了三天没核平。
我的做法很笨:不碰后台,只拿两份数据源对齐。一份是 AdSense 后台「付款 -> 交易」里导出的 CSV,字段里有日期、付款编号、收入、扣税、实付;另一份是 Gmail 里 Google 发来的付款通知邮件,同一批付款编号。用 Apps Script 跑,因为账号本身就在 Google Workspace 里,授权一步到位,不用另配 OAuth。
function reconcile() {
const ss = SpreadsheetApp.getActive();
const rows = ss.getSheetByName('transactions').getDataRange().getValues();
const head = rows.shift();
const iPay = head.indexOf('付款编号');
const iGross = head.indexOf('收入');
const iTax = head.indexOf('扣税');
const key = {};
rows.forEach(r => {
key[r[iPay]] = { gross: r[iGross], tax: r[iTax] };
});
const threads = GmailApp.search('from:payments-noreply@google.com newer_than:15m', 0, 50);
const out = [];
threads.forEach(t => t.getMessages().forEach(m => {
const body = m.getPlainBody();
const id = (body.match(/\b\d{4}-\d{6}-\d{6}\b/) || [])[0];
if (!id || !key[id]) return;
const amt = parseFloat((body.match(/TRY\s*([\d.,]+)/) || [])[1].replace(/\./g, '').replace(',', '.'));
out.push([id, key[id].gross, key[id].tax, amt, (key[id].gross - key[id].tax - amt).toFixed(2)]);
}));
ss.getSheetByName('diff').getRange(2, 1, out.length, 5).setValues(out);
}
跑完差异列基本都在 0.00–0.03 之间,只有三笔偏差超过 1 TRY,查下来是付款编号在邮件正文里被换行截断,正则没匹配上,补一个 \\s* 就对了。
为什么用这个路子:AdSense 的扣税比例不是固定值,它跟当月收入结构、是否含 YouTube 分成、以及 W-8BEN 表里申报的税收协定条款都有关,所以拿「15%」去乘总收入必然对不平。真正能验的只有逐笔对账——付款编号是唯一稳定主键,邮件里的实付金额是银行侧的口径,两边一夹,差额就只剩汇率和四舍五入。代价是 Apps Script 单次执行有 6 分钟上限,Gmail 搜索一次最多取 50 个会话,14 个月要分几次跑;另外 getPlainBody() 对多语言邮件偶尔会丢表格,纯 HTML 邮件得改走 getBody() 再剥标签。
会失效的地方我也碰过:账号如果同时挂了多个付款资料,付款编号前缀会变,正则里的 \\d{6} 就不够;Workspace 管理员如果禁了 gmail.readonly,授权直接卡在同意页;还有一次会计把 CSV 用 Excel 另存成 UTF-8 BOM,首列表头带不可见字符,indexOf('付款编号') 返回 -1,整张表静默跳过——这个最坑,因为脚本不报错,只是 diff 页空白。
你们那边土耳其区账号的扣税比例,是按月波动还是全年一个固定值?我这边看到的是前者,想确认是不是只有我们这样。
0 comments
No comments yet — be the first.