中国本土应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

418 lines
20KB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # OpenERP, Open Source Management Solution
  5. # Copyright (C) 2016 武康开源软件(宣一敏).
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from odoo import api, fields, models, tools, _
  22. from lxml import etree
  23. from odoo.exceptions import UserError
  24. import base64
  25. import io
  26. import zipfile
  27. from datetime import date
  28. class AccountMove(models.Model):
  29. _inherit = 'account.move'
  30. """
  31. 客户生成销售xml,存附件,todo存nas
  32. 供应商生成xml,存附件
  33. 批量生成xml。
  34. """
  35. cn_invoice_type = fields.Many2one('cn.invoice.type', string='发票类型')
  36. cn_invoice_type_code = fields.Char(string='发票类型编码', related='cn_invoice_type.code', store=True)
  37. def action_to_cn_invoice(self):
  38. return {
  39. 'name': _('导出发票'),
  40. 'res_model': 'account.to.cn.invoice',
  41. 'view_mode': 'form',
  42. 'context': {
  43. 'active_model': 'account.move',
  44. 'active_ids': self.ids,
  45. },
  46. 'target': 'new',
  47. 'type': 'ir.actions.act_window',
  48. }
  49. def to_cn_invoice_dz_xml(self):
  50. # 清掉已开票,用于生成xml
  51. for line in self.invoice_line_ids:
  52. line.write({
  53. 'is_cn_invoice': False,
  54. })
  55. # 开始写xml
  56. business = etree.Element("business", comment=u"发票开具", id="FPKJ")
  57. self._to_dz_xml_top(business)
  58. tree = etree.ElementTree(business)
  59. xml_content = etree.tostring(tree, pretty_print=True, encoding="GBK")
  60. attachment = self.env['ir.attachment'].create({
  61. 'type': 'binary',
  62. 'name': '电子发票-%s.xml' % self.name,
  63. 'res_model': 'mail.compose.message',
  64. 'datas': base64.encodebytes(xml_content),
  65. })
  66. self.message_post(attachment_ids=[attachment.id])
  67. # filename = '电子发票-%s.xml' % self.name
  68. # return http.send_file(base64.encodebytes(xml_content), filename=filename, as_attachment=True)
  69. return xml_content
  70. def to_cn_invoice_xml(self):
  71. #清掉已开票,用于生成xml
  72. for line in self.invoice_line_ids:
  73. line.write({
  74. 'is_cn_invoice': False,
  75. })
  76. #开始写xml
  77. Kp = etree.Element('Kp')
  78. self._to_xml_top(Kp)
  79. tree = etree.ElementTree(Kp)
  80. xml_content = etree.tostring(tree, pretty_print=True, encoding="GBK")
  81. attachment = self.env['ir.attachment'].create({
  82. 'type': 'binary',
  83. 'name': '纸质发票-%s.xml' % self.name,
  84. 'res_model': 'mail.compose.message',
  85. 'datas': base64.encodebytes(xml_content),
  86. })
  87. self.message_post(attachment_ids=[attachment.id])
  88. return xml_content
  89. def _to_xml_top(self, Kp):
  90. # 处理xml发票张数
  91. # 处理XML头
  92. Version = etree.SubElement(Kp, 'Version')
  93. Version.text = '3.0'
  94. Fpxx = etree.SubElement(Kp, 'Fpxx')
  95. # 处理xml发票张数
  96. Zsl = etree.SubElement(Fpxx, 'Zsl') # 单据数量
  97. i = 0 #单据数量
  98. invoice = '%s'%(self.name)
  99. amount_top = self.env.company.invoice_top_amount
  100. all_invoiced = False
  101. while not all_invoiced:
  102. # 发票头
  103. if self.move_type == 'out_invoice': #销售发票
  104. fapiao_name = self.partner_id.name
  105. fapiao_vat = self.partner_id.vat
  106. fapiao_address = "%s%s%s %s"%(self.partner_id.city, self.partner_id.street, self.partner_id.street2, self.partner_id.phone)
  107. fapiao_bank = ''
  108. if self.partner_id.bank_ids:
  109. fapiao_bank = '%s %s' % (self.partner_id.bank_ids[0].bank_id.name, self.partner_id.bank_ids[0].acc_number)
  110. if self.move_type == 'in_invoice': #采购发票
  111. fapiao_name = self.env.company.name
  112. fapiao_vat = self.env.company.vat
  113. fapiao_address = "%s%s%s %s" % (
  114. self.env.company.city, self.env.company.street, self.env.company.street2, self.env.company.phone)
  115. fapiao_bank = ''
  116. if self.env.company.bank_ids:
  117. fapiao_bank = '%s %s' % (
  118. self.env.company.bank_ids[0].bank_id.name, self.env.company.bank_ids[0].acc_number)
  119. fapiao_note = self.ref or ''
  120. Fpsj = etree.SubElement(Fpxx, 'Fpsj')
  121. Fp = etree.SubElement(Fpsj, 'Fp')
  122. Djh = etree.SubElement(Fp, 'Djh') # 单据号
  123. Djh.text = invoice
  124. Spbmbbh = etree.SubElement(Fp, 'Spbmbbh') # 商品编码版本号
  125. Spbmbbh.text = '19.0'
  126. Hsbz = etree.SubElement(Fp, 'Hsbz') # 含税标志
  127. Hsbz.text = '0'
  128. Sgbz = etree.SubElement(Fp, 'Sgbz') # 含税标志
  129. Sgbz.text = '0'
  130. Gfmc = etree.SubElement(Fp, 'Gfmc') # 购方名称
  131. Gfmc.text = fapiao_name
  132. Gfsh = etree.SubElement(Fp, 'Gfsh') # 购方税号
  133. Gfsh.text = fapiao_vat
  134. Gfdzdh = etree.SubElement(Fp, 'Gfdzdh') # 购方地址电话
  135. Gfdzdh.text = fapiao_address
  136. Gfyhzh = etree.SubElement(Fp, 'Gfyhzh') # 购方银行帐号
  137. Gfyhzh.text = fapiao_bank
  138. Skr = etree.SubElement(Fp, 'Skr') # 收款人
  139. Skr.text = ''
  140. Fhr = etree.SubElement(Fp, 'Fhr') # 复核人
  141. Fhr.text = ''
  142. Bz = etree.SubElement(Fp, 'Bz') # 备注
  143. Bz.text = fapiao_note
  144. Spxx = etree.SubElement(Fp, 'Spxx')
  145. # 发票明细行
  146. all_invoiced = self._mixi(amount_top, Spxx)
  147. i += 1
  148. Djh.text = '%s%d'%(invoice, i)
  149. Zsl.text = str(i)
  150. def _mixi(self, amount_top, Spxx):
  151. # 明细计算内容,
  152. total_untaxed = i = 0
  153. all_invoiced = True
  154. for line in self.invoice_line_ids:
  155. if len(line.tax_ids.ids) > 1:
  156. raise UserError('多种税率无法开纸质发票!')
  157. if line.is_cn_invoice:
  158. continue
  159. total_untaxed += (line.credit or line.debit)
  160. if (line.credit or line.debit) > amount_top:
  161. raise UserError('系统不支持单行商品金额超上限!')
  162. if total_untaxed > amount_top:
  163. # todo 单行超开票上限拆分多张发票
  164. all_invoiced = False
  165. total_untaxed = total_untaxed - (line.credit or line.debit)
  166. continue
  167. else:
  168. Sph = etree.SubElement(Spxx, 'Sph')
  169. Kce = etree.SubElement(Sph, 'Kce') # 扣除额
  170. Kce.text = ''
  171. Spbm = etree.SubElement(Sph, 'Spbm') # 商品编码
  172. tax_category_id = line.product_id.tax_category_id or line.product_id.categ_id.tax_category_id
  173. if tax_category_id:
  174. Spbm.text = str(tax_category_id.code)
  175. else:
  176. raise UserError('未在产品/产品分类上设置税收编码!')
  177. Dj = etree.SubElement(Sph, 'Dj') # 单价
  178. tax_rate = 0
  179. price = line.price_unit
  180. price_include = False
  181. if line.tax_ids:
  182. tax_rate = round(line.tax_ids[0].amount/100,2)
  183. price_include = line.tax_ids[0].price_include
  184. if price_include:
  185. dj = '%s'%(price/(1+tax_rate/100))
  186. else:
  187. dj = '%s'%(price)
  188. Dj.text = dj
  189. Spmc = etree.SubElement(Sph, 'Spmc') # 商品名称
  190. Spmc.text = line.product_id.name
  191. Ggxh = etree.SubElement(Sph, 'Ggxh') # 规格型号
  192. Ggxh.text = '-'.join([tag.name for tag in line.product_id.product_tag_ids])
  193. Slv = etree.SubElement(Sph, 'Slv') # 税率
  194. Slv.text = '%s'%(tax_rate)
  195. Xh = etree.SubElement(Sph, 'Xh') # 序号
  196. i += 1
  197. Xh.text = '%d'%(i)
  198. Lslbz = etree.SubElement(Sph, 'Lslbz') # 零标识,0出口退税,1免税
  199. Lslbz.text = ''
  200. Syyhzcbz = etree.SubElement(Sph, 'Syyhzcbz') # 优惠政策标识:0不使用,1使用
  201. Syyhzcbz.text = '0'
  202. Sl = etree.SubElement(Sph, 'Sl') # 数量
  203. Sl.text = '%.2f'%(round(line.quantity, 2))
  204. Je = etree.SubElement(Sph, 'Je') # 金额
  205. Je.text = '%.2f'%(round((line.credit or line.debit), 2))
  206. Se = etree.SubElement(Sph, 'Se') # 税额
  207. if tax_rate:
  208. Se.text = '%.2f' % (line.price_total - round((line.credit or line.debit), 2))
  209. else:
  210. Se.text = '0'
  211. Yhzcsm = etree.SubElement(Sph, 'Yhzcsm') # 优惠政策说明
  212. Yhzcsm.text = ''
  213. Qyspbm = etree.SubElement(Sph, 'Qyspbm') # 企业商品编码
  214. Qyspbm.text = ''
  215. Jldw = etree.SubElement(Sph, 'Jldw') # 计量单位
  216. Jldw.text = line.product_uom_id.name
  217. line.write({
  218. 'is_cn_invoice': True,
  219. })
  220. return all_invoiced
  221. def _to_dz_xml_top(self, business):
  222. i = 0 # 单据数量
  223. invoice = '%s' % (self.name)
  224. amount_top = self.env.company.invoice_top_amount
  225. all_invoiced = False
  226. while not all_invoiced:
  227. if self.move_type == 'out_invoice': # 销售发票
  228. fapiao_no = '%s%d' % (invoice, i)
  229. fapiao_name = self.partner_id.name
  230. fapiao_vat = self.partner_id.vat
  231. fapiao_address = "%s%s%s %d" % (
  232. self.partner_id.city, self.partner_id.street, self.partner_id.street2, self.partner_id.phone)
  233. fapiao_bank = ''
  234. if self.partner_id.bank_ids:
  235. fapiao_bank = '%s %s' % (
  236. self.partner_id.bank_ids[0].bank_id.name, self.partner_id.bank_ids[0].acc_number)
  237. fapiao_note = self.ref or ''
  238. company_name = self.env.company.name
  239. company_vat = self.env.company.vat
  240. company_address = "%s%s%s %s" % (
  241. self.env.company.city, self.env.company.street, self.env.company.street2, self.env.company.phone)
  242. company_bank = ''
  243. if self.env.company.bank_ids:
  244. company_bank = '%s %s' % (
  245. self.env.company.bank_ids[0].bank_id.name, self.env.company.bank_ids[0].acc_number)
  246. # 发票头
  247. REQUEST_COMMON_FPKJ = etree.SubElement(business, 'REQUEST_COMMON_FPKJ')
  248. REQUEST_COMMON_FPKJ.set("class", "REQUEST_COMMON_FPKJ")
  249. COMMON_FPKJ_FPT = etree.SubElement(REQUEST_COMMON_FPKJ, 'COMMON_FPKJ_FPT')
  250. COMMON_FPKJ_FPT.set("class", "COMMON_FPKJ_FPT")
  251. FPQQLSH = etree.SubElement(COMMON_FPKJ_FPT, 'FPQQLSH') # 开票请求流水号
  252. FPQQLSH.text = fapiao_no
  253. KPLX = etree.SubElement(COMMON_FPKJ_FPT, 'KPLX') # 开票类型 0为蓝字,1为红字
  254. KPLX.text = '0'
  255. XSF_NSRSBH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_NSRSBH') # 销售方纳税人识别号
  256. XSF_NSRSBH.text = company_vat
  257. XSF_MC = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_MC') # 销售方名称
  258. XSF_MC.text = company_name
  259. XSF_DZDH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_DZDH') # 销售方地址、电话
  260. XSF_DZDH.text = company_address
  261. XSF_YHZH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_YHZH') # 销售方银行帐号
  262. XSF_YHZH.text = company_bank
  263. GMF_NSRSBH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_NSRSBH') # 购买主纳税人识别号
  264. GMF_NSRSBH.text = fapiao_vat
  265. GMF_MC = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_MC') # 购方名称
  266. GMF_MC.text = fapiao_name
  267. GMF_DZDH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_DZDH') # 购方地址、电话
  268. GMF_DZDH.text = fapiao_address
  269. GMF_YHZH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_YHZH') # 购方银行帐号
  270. GMF_YHZH.text = fapiao_bank
  271. KPR = etree.SubElement(COMMON_FPKJ_FPT, 'KPR') # 开票人
  272. KPR.text = ''
  273. SKR = etree.SubElement(COMMON_FPKJ_FPT, 'SKR') # 收款人
  274. SKR.text = ''
  275. FHR = etree.SubElement(COMMON_FPKJ_FPT, 'FHR') # 复核人
  276. FHR.text = ''
  277. YFP_DM = etree.SubElement(COMMON_FPKJ_FPT, 'YFP_DM') # 原发票代码,红字必须
  278. YFP_DM.text = ''
  279. YFP_HM = etree.SubElement(COMMON_FPKJ_FPT, 'YFP_HM') # 原发票号码,红字必须
  280. YFP_HM.text = ''
  281. BZ = etree.SubElement(COMMON_FPKJ_FPT, 'BZ') # 备注
  282. BMB_BBH = etree.SubElement(COMMON_FPKJ_FPT, 'BMB_BBH') # 版本号
  283. BMB_BBH.text = '18.0'
  284. JSHJ = etree.SubElement(COMMON_FPKJ_FPT, 'JSHJ') # 价税合计
  285. HJJE = etree.SubElement(COMMON_FPKJ_FPT, 'HJJE') # 合计金额(不含税)
  286. HJSE = etree.SubElement(COMMON_FPKJ_FPT, 'HJSE') # 合计税额
  287. HSBZ = etree.SubElement(COMMON_FPKJ_FPT, 'HSBZ') # 税率
  288. HJSE.text = '0.00'
  289. COMMON_FPKJ_XMXXS = etree.SubElement(REQUEST_COMMON_FPKJ, 'COMMON_FPKJ_XMXXS')
  290. COMMON_FPKJ_XMXXS.set("class", "COMMON_FPKJ_XMXX")
  291. COMMON_FPKJ_XMXXS.set("size", "1")
  292. # 发票明细行
  293. all_invoiced = self._dzmixi(COMMON_FPKJ_XMXXS, amount_top)
  294. BZ.text = fapiao_note
  295. HJJE.text = '%.2f'%self.amount_untaxed_signed
  296. JSHJ.text = '%.2f'%self.amount_total_signed
  297. HJSE.text = '%.2f'%(self.amount_total_signed - self.amount_untaxed_signed)
  298. HSBZ.text = '0'
  299. def _dzmixi(self, COMMON_FPKJ_XMXXS, amount_top):
  300. total_untaxed = 0
  301. all_invoiced = True
  302. for line in self.invoice_line_ids:
  303. if len(line.tax_ids.ids) > 1:
  304. raise UserError('多种税率无法开纸质发票!')
  305. if line.is_cn_invoice:
  306. continue
  307. total_untaxed += (line.credit or line.debit)
  308. if (line.credit or line.debit) > amount_top:
  309. raise UserError('系统不支持单行商品金额超上限!')
  310. if total_untaxed > amount_top:
  311. # todo 单行超开票上限拆分多张发票
  312. all_invoiced = False
  313. total_untaxed = total_untaxed - (line.credit or line.debit)
  314. continue
  315. else:
  316. amount = (line.credit or line.debit) # 成交人民币
  317. COMMON_FPKJ_XMXX = etree.SubElement(COMMON_FPKJ_XMXXS, 'COMMON_FPKJ_XMXX')
  318. FPHXZ = etree.SubElement(COMMON_FPKJ_XMXX, 'FPHXZ') # 发票行性质,0正常行,1折扣行,2被折扣行
  319. FPHXZ.text = '0'
  320. XMMC = etree.SubElement(COMMON_FPKJ_XMXX, 'XMMC') # 商品名称
  321. XMMC.text = line.product_id.name
  322. GGXH = etree.SubElement(COMMON_FPKJ_XMXX, 'GGXH') # 规格型号
  323. GGXH.text = '-'.join([tag.name for tag in line.product_id.product_tag_ids])
  324. DW = etree.SubElement(COMMON_FPKJ_XMXX, 'DW') # 计量单位
  325. DW.text = line.product_uom_id.name
  326. SPBM = etree.SubElement(COMMON_FPKJ_XMXX, 'SPBM') # 税收编码
  327. tax_category_id = line.product_id.tax_category_id or line.product_id.categ_id.tax_category_id
  328. if tax_category_id:
  329. SPBM.text = str(tax_category_id.code)
  330. else:
  331. raise UserError('未在产品/产品分类上设置税收编码!')
  332. ZXBM = etree.SubElement(COMMON_FPKJ_XMXX, 'ZXBM') # 企业编码
  333. ZXBM.text = ''
  334. YHZCBS = etree.SubElement(COMMON_FPKJ_XMXX, 'YHZCBS') # 优惠政策标识:0不使用,1使用
  335. YHZCBS.text = '0'
  336. LSLBS = etree.SubElement(COMMON_FPKJ_XMXX, 'LSLBS') # 零标识,0出口退税,1免税
  337. ZZSTSGL = etree.SubElement(COMMON_FPKJ_XMXX, 'ZZSTSGL') # 优惠政策说明??
  338. XMSL = etree.SubElement(COMMON_FPKJ_XMXX, 'XMSL') # 数量
  339. XMSL.text = '%.2f'%(round(line.quantity, 2))
  340. XMDJ = etree.SubElement(COMMON_FPKJ_XMXX, 'XMDJ') # 单价
  341. tax_rate = 0
  342. price = line.price_unit
  343. price_include = False
  344. if line.tax_ids:
  345. tax_rate = round(line.tax_ids[0].amount / 100, 2)
  346. price_include = line.tax_ids[0].price_include
  347. if price_include:
  348. dj = '%s' % (price / (1 + tax_rate / 100))
  349. else:
  350. dj = '%s' % (price)
  351. XMDJ.text = dj
  352. XMJE = etree.SubElement(COMMON_FPKJ_XMXX, 'XMJE') # 金额
  353. XMJE.text = '%.2f'%(round((line.credit or line.debit), 2))
  354. SE = etree.SubElement(COMMON_FPKJ_XMXX, 'SE') # 税额
  355. if tax_rate:
  356. SE.text = '%.2f' % (line.price_total - round((line.credit or line.debit), 2))
  357. else:
  358. SE.text = '0'
  359. SL = etree.SubElement(COMMON_FPKJ_XMXX, 'SL') # 税率
  360. SL.text = '%s'%(tax_rate)
  361. KCE = etree.SubElement(COMMON_FPKJ_XMXX, 'KCE') # 扣除额
  362. KCE.text = '0'
  363. return all_invoiced
  364. class AccountToCnInvoice(models.TransientModel):
  365. _name = 'account.to.cn.invoice'
  366. _description = '开发票'
  367. name = fields.Char('File Name', readonly=True)
  368. cn_invoice_type = fields.Many2one('cn.invoice.type', string='发票类型')
  369. data = fields.Binary('File', readonly=True, attachment=False)
  370. state = fields.Selection([('draft', 'draft'), ('done', 'done')], default='draft')
  371. def act_getfile(self):
  372. invoice_ids = self.env['account.move'].browse(self._context.get('active_ids'))
  373. stream = io.BytesIO()
  374. with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as doc_zip:
  375. i = 1
  376. for invoice in invoice_ids:
  377. if self.cn_invoice_type.code in ['zp', 'pp']:
  378. invoice_xml = invoice.to_cn_invoice_xml()
  379. name = '纸质发票/%s.xml' % invoice.name.replace('/', '-')
  380. elif self.cn_invoice_type.code in ['dzzp', 'dzfp']:
  381. invoice_xml = invoice.to_cn_invoice_dz_xml()
  382. name = '电子发票/%s.xml' % invoice.name.replace('/', '-')
  383. doc_zip.writestr(name, invoice_xml)
  384. i += 1
  385. name = "%s.zip" % (date.today())
  386. self.write({
  387. 'data': base64.encodebytes(stream.getvalue()),
  388. 'name': name,
  389. 'state': 'done',
  390. })
  391. return {
  392. 'type': 'ir.actions.act_window',
  393. 'res_model': 'account.to.cn.invoice',
  394. 'view_mode': 'form',
  395. 'res_id': self.id,
  396. 'views': [(False, 'form')],
  397. 'target': 'new',
  398. }
上海开阖软件有限公司 沪ICP备12045867号-1