|
- # -*- coding: utf-8 -*-
- ##############################################################################
- #
- # OpenERP, Open Source Management Solution
- # Copyright (C) 2016 武康开源软件(宣一敏).
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as
- # published by the Free Software Foundation, either version 3 of the
- # License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- #
- ##############################################################################
-
- from odoo import api, fields, models, tools, _
- from lxml import etree
- from odoo.exceptions import UserError
- import base64
- import io
- import zipfile
- from datetime import date
-
- class SpecificBusinessTypes(models.Model):
- _name = 'specific.business.types'
- _description = '特定业务类型'
- _rec_name= 'code'
-
- code = fields.Char('编码')
- name = fields.Char('名称')
- active = fields.Boolean(default=True)
-
- class IdType(models.Model):
- _name = 'id.type'
- _description = '证件类型'
- _rec_name= 'code'
-
- code = fields.Char('编码')
- name = fields.Char('名称')
- active = fields.Boolean(default=True)
-
- class AbandonedCollection(models.Model):
- _name = 'abandoned.collection'
- _description = '放弃享受减按1%征收率原因'
- _rec_name= 'code'
-
- code = fields.Char('编码')
- name = fields.Char('名称')
- active = fields.Boolean(default=True)
-
- class AccountMove(models.Model):
- _inherit = 'account.move'
-
- """
- 客户生成销售xml,存附件
- 供应商生成xml,存附件
- 批量生成xml。
- """
-
- cn_invoice_type = fields.Many2one('cn.invoice.type', string='发票类型')
- cn_invoice_type_code = fields.Char(string='发票类型编码', related='cn_invoice_type.code', store=True)
- specific_business_types = fields.Many2one('specific.business.types', string='特定业务类型')
- is_taxed = fields.Boolean('是否含税', default=False, compute='_compute_is_taxed', store=True)
- is_natural_person = fields.Boolean('受票方自然人标识', default=False, compute='_compute_is_company', store=True)
- id_type = fields.Many2one('id.type', string='证件类型')
- is_show_buy_bank = fields.Boolean('是否展示购买方银行账号', default=True, compute='_compute_show_bank', store=True)
- is_show_sell_bank = fields.Boolean('是否展示销售方银行账号', default=True, compute='_compute_show_bank', store=True)
- abandoned_collection = fields.Many2one('abandoned.collection', string='放弃享受减按1%征收率原因')
- payee = fields.Char(string='收款人')
- checker = fields.Char(string='收款人')
-
- @api.depends('partner_id', 'partner_id.company_type')
- def _compute_is_company(self):
- self.ensure_one()
- if self.partner_id and self.partner_id.company_type == 'company':
- self.is_natural_person = False
- else:
- self.is_natural_person = True
-
- @api.depends('move_type', 'partner_id', 'partner_id.bank_ids', 'partner_bank_id')
- def _compute_show_bank(self):
- self.ensure_one()
- if self.move_type == 'out_invoice': # 销售发票
- if self.partner_id.bank_ids:
- self.is_show_buy_bank = True
- else:
- self.is_show_buy_bank = False
- if self.partner_bank_id:
- self.is_show_sell_bank = True
- else:
- self.is_show_sell_bank = False
-
- else:
- if self.partner_id.bank_ids:
- self.is_show_sell_bank = True
- else:
- self.is_show_sell_bank = False
- if self.env.company.bank_ids:
- self.is_show_buy_bank = True
- else:
- self.is_show_buy_bank = False
-
- @api.depends("invoice_line_ids", "invoice_line_ids.tax_ids", "invoice_line_ids.tax_ids.price_include")
- def _compute_is_taxed(self):
- self.ensure_one()
- line_taxed = []
- for line in self.invoice_line_ids:
- line_taxed = line_taxed + [tax.price_include for tax in line.tax_ids]
-
- taxed = list(set(line_taxed))
- if len(taxed) > 1:
- raise UserError('明细行不可以即含税又不含税')
- if taxed:
- if taxed[0] == True:
- self.is_taxed = True
- else:
- self.is_taxed = False
-
-
-
- def action_to_cn_invoice(self):
- return {
- 'name': _('导出发票'),
- 'res_model': 'account.to.cn.invoice',
- 'view_mode': 'form',
- 'context': {
- 'active_model': 'account.move',
- 'active_ids': self.ids,
- },
- 'target': 'new',
- 'type': 'ir.actions.act_window',
- }
-
- def to_cn_invoice_dz_xml(self):
- # 清掉已开票,用于生成xml
- for line in self.invoice_line_ids:
- line.write({
- 'is_cn_invoice': False,
- })
- # 开始写xml
- business = etree.Element("business", comment=u"发票开具", id="FPKJ")
- self._to_dz_xml_top(business)
- tree = etree.ElementTree(business)
- xml_content = etree.tostring(tree, pretty_print=True, encoding="GBK")
- attachment = self.env['ir.attachment'].create({
- 'type': 'binary',
- 'name': '电子发票-%s.xml' % self.name,
- 'res_model': 'mail.compose.message',
- 'datas': base64.encodebytes(xml_content),
- 'company_id': self.company_id.id,
- })
- self.message_post(attachment_ids=[attachment.id])
- # filename = '电子发票-%s.xml' % self.name
- # return http.send_file(base64.encodebytes(xml_content), filename=filename, as_attachment=True)
- return xml_content
-
- def to_cn_invoice_xml(self):
- #清掉已开票,用于生成xml
- for line in self.invoice_line_ids:
- line.write({
- 'is_cn_invoice': False,
- })
- #开始写xml
- Kp = etree.Element('Kp')
- self._to_xml_top(Kp)
- tree = etree.ElementTree(Kp)
- xml_content = etree.tostring(tree, pretty_print=True, encoding="GBK")
- attachment = self.env['ir.attachment'].create({
- 'type': 'binary',
- 'name': '纸质发票-%s.xml' % self.name,
- 'res_model': 'mail.compose.message',
- 'datas': base64.encodebytes(xml_content),
- 'company_id': self.company_id.id,
- })
- self.message_post(attachment_ids=[attachment.id])
- return xml_content
-
- def _to_xml_top(self, Kp):
- # 处理xml发票张数
- # 处理XML头
- Version = etree.SubElement(Kp, 'Version')
- Version.text = '3.0'
- Fpxx = etree.SubElement(Kp, 'Fpxx')
- # 处理xml发票张数
- Zsl = etree.SubElement(Fpxx, 'Zsl') # 单据数量
- i = 0 #单据数量
- invoice = '%s'%(self.name)
- amount_top = self.env.company.invoice_top_amount
- all_invoiced = False
- while not all_invoiced:
- # 发票头
- if self.move_type == 'out_invoice': #销售发票
- fapiao_name = self.partner_id.name
- fapiao_vat = self.partner_id.vat
- fapiao_address = "%s%s%s %s"%(self.partner_id.city, self.partner_id.street, self.partner_id.street2, self.partner_id.phone)
- fapiao_bank = ''
- if self.partner_id.bank_ids:
- fapiao_bank = '%s %s' % (self.partner_id.bank_ids[0].bank_id.name, self.partner_id.bank_ids[0].acc_number)
-
- if self.move_type == 'in_invoice': #采购发票
- fapiao_name = self.env.company.name
- fapiao_vat = self.env.company.vat
- fapiao_address = "%s%s%s %s" % (
- self.env.company.city, self.env.company.street, self.env.company.street2, self.env.company.phone)
- fapiao_bank = ''
- if self.env.company.bank_ids:
- fapiao_bank = '%s %s' % (
- self.env.company.bank_ids[0].bank_id.name, self.env.company.bank_ids[0].acc_number)
- fapiao_note = self.ref or ''
-
- Fpsj = etree.SubElement(Fpxx, 'Fpsj')
- Fp = etree.SubElement(Fpsj, 'Fp')
- Djh = etree.SubElement(Fp, 'Djh') # 单据号
- Djh.text = invoice
- Spbmbbh = etree.SubElement(Fp, 'Spbmbbh') # 商品编码版本号
- Spbmbbh.text = '19.0'
- Hsbz = etree.SubElement(Fp, 'Hsbz') # 含税标志
- Hsbz.text = '0'
- Sgbz = etree.SubElement(Fp, 'Sgbz') # 含税标志
- Sgbz.text = '0'
- Gfmc = etree.SubElement(Fp, 'Gfmc') # 购方名称
- Gfmc.text = fapiao_name
- Gfsh = etree.SubElement(Fp, 'Gfsh') # 购方税号
- Gfsh.text = fapiao_vat
- Gfdzdh = etree.SubElement(Fp, 'Gfdzdh') # 购方地址电话
- Gfdzdh.text = fapiao_address
- Gfyhzh = etree.SubElement(Fp, 'Gfyhzh') # 购方银行帐号
- Gfyhzh.text = fapiao_bank
- Skr = etree.SubElement(Fp, 'Skr') # 收款人
- Skr.text = ''
- Fhr = etree.SubElement(Fp, 'Fhr') # 复核人
- Fhr.text = ''
- Bz = etree.SubElement(Fp, 'Bz') # 备注
- Bz.text = fapiao_note
- Spxx = etree.SubElement(Fp, 'Spxx')
- # 发票明细行
- all_invoiced = self._mixi(amount_top, Spxx)
- i += 1
- Djh.text = '%s%d'%(invoice, i)
- Zsl.text = str(i)
-
- def _mixi(self, amount_top, Spxx):
- # 明细计算内容,
- total_untaxed = i = 0
- all_invoiced = True
- for line in self.invoice_line_ids:
- if len(line.tax_ids.ids) > 1:
- raise UserError('多种税率无法开纸质发票!')
- if line.is_cn_invoice:
- continue
- total_untaxed += (line.credit or line.debit)
- if (line.credit or line.debit) > amount_top:
- raise UserError('系统不支持单行商品金额超上限!')
- if total_untaxed > amount_top:
- # todo 单行超开票上限拆分多张发票
- all_invoiced = False
- total_untaxed = total_untaxed - (line.credit or line.debit)
- continue
- else:
- Sph = etree.SubElement(Spxx, 'Sph')
- Kce = etree.SubElement(Sph, 'Kce') # 扣除额
- Kce.text = ''
- Spbm = etree.SubElement(Sph, 'Spbm') # 商品编码
- tax_category_id = line.product_id.tax_category_id or line.product_id.categ_id.tax_category_id
- if tax_category_id:
- Spbm.text = str(tax_category_id.code)
- else:
- raise UserError('未在产品/产品分类上设置税收编码!')
- Dj = etree.SubElement(Sph, 'Dj') # 单价
- tax_rate = 0
- price = line.price_unit
- price_include = False
- if line.tax_ids:
- tax_rate = round(line.tax_ids[0].amount/100,2)
- price_include = line.tax_ids[0].price_include
- if price_include:
- dj = '%s'%(price/(1+tax_rate/100))
- else:
- dj = '%s'%(price)
- Dj.text = dj
- Spmc = etree.SubElement(Sph, 'Spmc') # 商品名称
- Spmc.text = line.product_id.name
- Ggxh = etree.SubElement(Sph, 'Ggxh') # 规格型号
- Ggxh.text = '-'.join([tag.name for tag in line.product_id.product_tag_ids])
- Slv = etree.SubElement(Sph, 'Slv') # 税率
- Slv.text = '%s'%(tax_rate)
- Xh = etree.SubElement(Sph, 'Xh') # 序号
- i += 1
- Xh.text = '%d'%(i)
- Lslbz = etree.SubElement(Sph, 'Lslbz') # 零标识,0出口退税,1免税
- Lslbz.text = ''
- Syyhzcbz = etree.SubElement(Sph, 'Syyhzcbz') # 优惠政策标识:0不使用,1使用
- Syyhzcbz.text = '0'
- Sl = etree.SubElement(Sph, 'Sl') # 数量
- Sl.text = '%.2f'%(round(line.quantity, 2))
- Je = etree.SubElement(Sph, 'Je') # 金额
- Je.text = '%.2f'%(round((line.credit or line.debit), 2))
- Se = etree.SubElement(Sph, 'Se') # 税额
- if tax_rate:
- Se.text = '%.2f' % (line.price_total - round((line.credit or line.debit), 2))
- else:
- Se.text = '0'
- Yhzcsm = etree.SubElement(Sph, 'Yhzcsm') # 优惠政策说明
- Yhzcsm.text = ''
- Qyspbm = etree.SubElement(Sph, 'Qyspbm') # 企业商品编码
- Qyspbm.text = ''
- Jldw = etree.SubElement(Sph, 'Jldw') # 计量单位
- Jldw.text = line.product_uom_id.name
-
- line.write({
- 'is_cn_invoice': True,
- })
- return all_invoiced
-
- def _to_dz_xml_top(self, business):
- i = 0 # 单据数量
- invoice = '%s' % (self.name)
- amount_top = self.env.company.invoice_top_amount
- all_invoiced = False
- while not all_invoiced:
- if self.move_type == 'out_invoice': # 销售发票
- fapiao_no = '%s%d' % (invoice, i)
- fapiao_name = self.partner_id.name
- fapiao_vat = self.partner_id.vat
- fapiao_address = "%s%s%s %d" % (
- self.partner_id.city, self.partner_id.street, self.partner_id.street2, self.partner_id.phone)
- fapiao_bank = ''
- if self.partner_id.bank_ids:
- fapiao_bank = '%s %s' % (
- self.partner_id.bank_ids[0].bank_id.name, self.partner_id.bank_ids[0].acc_number)
- fapiao_note = self.ref or ''
-
- company_name = self.env.company.name
- company_vat = self.env.company.vat
- company_address = "%s%s%s %s" % (
- self.env.company.city, self.env.company.street, self.env.company.street2, self.env.company.phone)
- company_bank = ''
- if self.env.company.bank_ids:
- company_bank = '%s %s' % (
- self.env.company.bank_ids[0].bank_id.name, self.env.company.bank_ids[0].acc_number)
- # 发票头
- REQUEST_COMMON_FPKJ = etree.SubElement(business, 'REQUEST_COMMON_FPKJ')
- REQUEST_COMMON_FPKJ.set("class", "REQUEST_COMMON_FPKJ")
- COMMON_FPKJ_FPT = etree.SubElement(REQUEST_COMMON_FPKJ, 'COMMON_FPKJ_FPT')
- COMMON_FPKJ_FPT.set("class", "COMMON_FPKJ_FPT")
- FPQQLSH = etree.SubElement(COMMON_FPKJ_FPT, 'FPQQLSH') # 开票请求流水号
- FPQQLSH.text = fapiao_no
- KPLX = etree.SubElement(COMMON_FPKJ_FPT, 'KPLX') # 开票类型 0为蓝字,1为红字
- KPLX.text = '0'
- XSF_NSRSBH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_NSRSBH') # 销售方纳税人识别号
- XSF_NSRSBH.text = company_vat
- XSF_MC = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_MC') # 销售方名称
- XSF_MC.text = company_name
- XSF_DZDH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_DZDH') # 销售方地址、电话
- XSF_DZDH.text = company_address
- XSF_YHZH = etree.SubElement(COMMON_FPKJ_FPT, 'XSF_YHZH') # 销售方银行帐号
- XSF_YHZH.text = company_bank
- GMF_NSRSBH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_NSRSBH') # 购买主纳税人识别号
- GMF_NSRSBH.text = fapiao_vat
- GMF_MC = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_MC') # 购方名称
- GMF_MC.text = fapiao_name
- GMF_DZDH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_DZDH') # 购方地址、电话
- GMF_DZDH.text = fapiao_address
- GMF_YHZH = etree.SubElement(COMMON_FPKJ_FPT, 'GMF_YHZH') # 购方银行帐号
- GMF_YHZH.text = fapiao_bank
- KPR = etree.SubElement(COMMON_FPKJ_FPT, 'KPR') # 开票人
- KPR.text = ''
- SKR = etree.SubElement(COMMON_FPKJ_FPT, 'SKR') # 收款人
- SKR.text = ''
- FHR = etree.SubElement(COMMON_FPKJ_FPT, 'FHR') # 复核人
- FHR.text = ''
- YFP_DM = etree.SubElement(COMMON_FPKJ_FPT, 'YFP_DM') # 原发票代码,红字必须
- YFP_DM.text = ''
- YFP_HM = etree.SubElement(COMMON_FPKJ_FPT, 'YFP_HM') # 原发票号码,红字必须
- YFP_HM.text = ''
- BZ = etree.SubElement(COMMON_FPKJ_FPT, 'BZ') # 备注
- BMB_BBH = etree.SubElement(COMMON_FPKJ_FPT, 'BMB_BBH') # 版本号
- BMB_BBH.text = '18.0'
- JSHJ = etree.SubElement(COMMON_FPKJ_FPT, 'JSHJ') # 价税合计
- HJJE = etree.SubElement(COMMON_FPKJ_FPT, 'HJJE') # 合计金额(不含税)
- HJSE = etree.SubElement(COMMON_FPKJ_FPT, 'HJSE') # 合计税额
- HSBZ = etree.SubElement(COMMON_FPKJ_FPT, 'HSBZ') # 税率
- HJSE.text = '0.00'
- COMMON_FPKJ_XMXXS = etree.SubElement(REQUEST_COMMON_FPKJ, 'COMMON_FPKJ_XMXXS')
- COMMON_FPKJ_XMXXS.set("class", "COMMON_FPKJ_XMXX")
- COMMON_FPKJ_XMXXS.set("size", "1")
- # 发票明细行
- all_invoiced = self._dzmixi(COMMON_FPKJ_XMXXS, amount_top)
- BZ.text = fapiao_note
- HJJE.text = '%.2f'%self.amount_untaxed_signed
- JSHJ.text = '%.2f'%self.amount_total_signed
- HJSE.text = '%.2f'%(self.amount_total_signed - self.amount_untaxed_signed)
- HSBZ.text = '0'
-
- def _dzmixi(self, COMMON_FPKJ_XMXXS, amount_top):
- total_untaxed = 0
- all_invoiced = True
- for line in self.invoice_line_ids:
- if len(line.tax_ids.ids) > 1:
- raise UserError('多种税率无法开纸质发票!')
- if line.is_cn_invoice:
- continue
- total_untaxed += (line.credit or line.debit)
- if (line.credit or line.debit) > amount_top:
- raise UserError('系统不支持单行商品金额超上限!')
- if total_untaxed > amount_top:
- # todo 单行超开票上限拆分多张发票
- all_invoiced = False
- total_untaxed = total_untaxed - (line.credit or line.debit)
- continue
- else:
- amount = (line.credit or line.debit) # 成交人民币
- COMMON_FPKJ_XMXX = etree.SubElement(COMMON_FPKJ_XMXXS, 'COMMON_FPKJ_XMXX')
- FPHXZ = etree.SubElement(COMMON_FPKJ_XMXX, 'FPHXZ') # 发票行性质,0正常行,1折扣行,2被折扣行
- FPHXZ.text = '0'
- XMMC = etree.SubElement(COMMON_FPKJ_XMXX, 'XMMC') # 商品名称
- XMMC.text = line.product_id.name
- GGXH = etree.SubElement(COMMON_FPKJ_XMXX, 'GGXH') # 规格型号
- GGXH.text = '-'.join([tag.name for tag in line.product_id.product_tag_ids])
- DW = etree.SubElement(COMMON_FPKJ_XMXX, 'DW') # 计量单位
- DW.text = line.product_uom_id.name
- SPBM = etree.SubElement(COMMON_FPKJ_XMXX, 'SPBM') # 税收编码
- tax_category_id = line.product_id.tax_category_id or line.product_id.categ_id.tax_category_id
- if tax_category_id:
- SPBM.text = str(tax_category_id.code)
- else:
- raise UserError('未在产品/产品分类上设置税收编码!')
- ZXBM = etree.SubElement(COMMON_FPKJ_XMXX, 'ZXBM') # 企业编码
- ZXBM.text = ''
- YHZCBS = etree.SubElement(COMMON_FPKJ_XMXX, 'YHZCBS') # 优惠政策标识:0不使用,1使用
- YHZCBS.text = '0'
- LSLBS = etree.SubElement(COMMON_FPKJ_XMXX, 'LSLBS') # 零标识,0出口退税,1免税
- ZZSTSGL = etree.SubElement(COMMON_FPKJ_XMXX, 'ZZSTSGL') # 优惠政策说明??
- XMSL = etree.SubElement(COMMON_FPKJ_XMXX, 'XMSL') # 数量
- XMSL.text = '%.2f'%(round(line.quantity, 2))
- XMDJ = etree.SubElement(COMMON_FPKJ_XMXX, 'XMDJ') # 单价
- tax_rate = 0
- price = line.price_unit
- price_include = False
- if line.tax_ids:
- tax_rate = round(line.tax_ids[0].amount / 100, 2)
- price_include = line.tax_ids[0].price_include
- if price_include:
- dj = '%s' % (price / (1 + tax_rate / 100))
- else:
- dj = '%s' % (price)
- XMDJ.text = dj
- XMJE = etree.SubElement(COMMON_FPKJ_XMXX, 'XMJE') # 金额
- XMJE.text = '%.2f'%(round((line.credit or line.debit), 2))
- SE = etree.SubElement(COMMON_FPKJ_XMXX, 'SE') # 税额
- if tax_rate:
- SE.text = '%.2f' % (line.price_total - round((line.credit or line.debit), 2))
- else:
- SE.text = '0'
- SL = etree.SubElement(COMMON_FPKJ_XMXX, 'SL') # 税率
- SL.text = '%s'%(tax_rate)
- KCE = etree.SubElement(COMMON_FPKJ_XMXX, 'KCE') # 扣除额
- KCE.text = '0'
- return all_invoiced
-
- class AccountToCnInvoice(models.TransientModel):
- _name = 'account.to.cn.invoice'
- _description = '开发票'
-
- name = fields.Char('File Name', readonly=True)
- is_new_type = fields.Boolean('开全电发票', default=True)
- cn_invoice_type = fields.Many2one('cn.invoice.type', string='发票类型')
-
- data = fields.Binary('File', readonly=True, attachment=False)
- img = fields.Binary('图片', attachment=True)
- is_updata = fields.Boolean('开始上传', default=True)
- state = fields.Selection([('draft', 'draft'), ('done', 'done'), ('end', 'end')], default='draft')
-
- def export_excel(self):
- pass
-
- def invoice_updata(self):
- pass
-
- def confirm_updata(self):
- pass
-
- def act_getfile(self):
- invoice_ids = self.env['account.move'].browse(self._context.get('active_ids'))
- stream = io.BytesIO()
- with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as doc_zip:
- i = 1
- for invoice in invoice_ids:
- if self.cn_invoice_type.code in ['zp', 'pp']:
- invoice_xml = invoice.to_cn_invoice_xml()
- name = '纸质发票/%s.xml' % invoice.name.replace('/', '-')
- elif self.cn_invoice_type.code in ['dzzp', 'dzfp']:
- invoice_xml = invoice.to_cn_invoice_dz_xml()
- name = '电子发票/%s.xml' % invoice.name.replace('/', '-')
- doc_zip.writestr(name, invoice_xml)
- i += 1
- name = "%s.zip" % (date.today())
- self.write({
- 'data': base64.encodebytes(stream.getvalue()),
- 'name': name,
- 'state': 'done',
- })
- return {
- 'type': 'ir.actions.act_window',
- 'res_model': 'account.to.cn.invoice',
- 'view_mode': 'form',
- 'res_id': self.id,
- 'views': [(False, 'form')],
- 'target': 'new',
- }
|