# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import xml.etree.ElementTree as ET from collections import namedtuple from odoo import api, exceptions, fields, models, _ INTRASTAT_XMLNS = 'http://www.onegate.eu/2010-01-01' class XmlDeclaration(models.TransientModel): """ Intrastat XML Declaration """ _name = "l10n_be_intrastat_xml.xml_decl" _description = 'Intrastat XML Declaration' def _default_get_month(self): return fields.Date.from_string(fields.Date.context_today(self)).strftime('%m') def _default_get_year(self): return fields.Date.from_string(fields.Date.context_today(self)).strftime('%Y') name = fields.Char(string='File Name', default='intrastat.xml') month = fields.Selection([('01', 'January'), ('02', 'February'), ('03', 'March'), ('04', 'April'), ('05', 'May'), ('06', 'June'), ('07', 'July'), ('08', 'August'), ('09', 'September'), ('10', 'October'), ('11', 'November'), ('12', 'December')], string='Month', required=True, default=_default_get_month) year = fields.Char(size=4, required=True, default=_default_get_year) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) arrivals = fields.Selection([('be-exempt', 'Exempt'), ('be-standard', 'Standard'), ('be-extended', 'Extended')], required=True, default='be-standard') dispatches = fields.Selection([('be-exempt', 'Exempt'), ('be-standard', 'Standard'), ('be-extended', 'Extended')], required=True, default='be-standard') file_save = fields.Binary(string='Intrastat Report File', readonly=True) state = fields.Selection([('draft', 'Draft'), ('download', 'Download')], default='draft') @api.model def _company_warning(self, translated_msg): """ Raise a error with custom message, asking user to configure company settings """ raise exceptions.RedirectWarning( translated_msg, self.env.ref('base.action_res_company_form').id, _('Go to company configuration screen')) @api.multi def create_xml(self): """Creates xml that is to be exported and sent to estate for partner vat intra. :return: Value for next action. :rtype: dict """ self.ensure_one() company = self.company_id if not (company.partner_id and company.partner_id.country_id and company.partner_id.country_id.id): self._company_warning(_('The country of your company is not set, ' 'please make sure to configure it first.')) if not company.company_registry: self._company_warning(_('The registry number of your company is not set, ' 'please make sure to configure it first.')) if len(self.year) != 4: raise exceptions.Warning(_('Year must be 4 digits number (YYYY)')) #Create root declaration decl = ET.Element('DeclarationReport') decl.set('xmlns', INTRASTAT_XMLNS) #Add Administration elements admin = ET.SubElement(decl, 'Administration') fromtag = ET.SubElement(admin, 'From') fromtag.text = company.company_registry fromtag.set('declarerType', 'KBO') ET.SubElement(admin, 'To').text = "NBB" ET.SubElement(admin, 'Domain').text = "SXX" if self.arrivals == 'be-standard': decl.append(self.sudo()._get_lines(dispatchmode=False, extendedmode=False)) elif self.arrivals == 'be-extended': decl.append(self.sudo()._get_lines(dispatchmode=False, extendedmode=True)) if self.dispatches == 'be-standard': decl.append(self.sudo()._get_lines(dispatchmode=True, extendedmode=False)) elif self.dispatches == 'be-extended': decl.append(self.sudo()._get_lines(dispatchmode=True, extendedmode=True)) #Get xml string with declaration data_file = ET.tostring(decl, encoding='UTF-8', method='xml') #change state of the wizard self.write({'name': 'intrastat_%s%s.xml' % (self.year, self.month), 'file_save': base64.encodestring(data_file), 'state': 'download'}) return { 'name': _('Save'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'l10n_be_intrastat_xml.xml_decl', 'type': 'ir.actions.act_window', 'target': 'new', 'res_id': self.id, } @api.multi def _get_lines(self, dispatchmode=False, extendedmode=False): company = self.company_id IntrastatRegion = self.env['l10n_be_intrastat.region'] if dispatchmode: mode1 = 'out_invoice' mode2 = 'in_refund' declcode = "29" else: mode1 = 'in_invoice' mode2 = 'out_refund' declcode = "19" decl = ET.Element('Report') if not extendedmode: decl.set('code', 'EX%sS' % declcode) else: decl.set('code', 'EX%sE' % declcode) decl.set('date', '%s-%s' % (self.year, self.month)) datas = ET.SubElement(decl, 'Data') if not extendedmode: datas.set('form', 'EXF%sS' % declcode) else: datas.set('form', 'EXF%sE' % declcode) datas.set('close', 'true') intrastatkey = namedtuple("intrastatkey", ['EXTRF', 'EXCNT', 'EXTTA', 'EXREG', 'EXGO', 'EXTPC', 'EXDELTRM']) entries = {} query = """ SELECT inv_line.id FROM account_invoice_line inv_line JOIN account_invoice inv ON inv_line.invoice_id=inv.id LEFT JOIN res_country ON res_country.id = inv.intrastat_country_id LEFT JOIN res_partner ON res_partner.id = inv.partner_id LEFT JOIN res_country countrypartner ON countrypartner.id = res_partner.country_id JOIN product_product ON inv_line.product_id=product_product.id JOIN product_template ON product_product.product_tmpl_id=product_template.id WHERE inv.state IN ('open','paid') AND inv.company_id=%s AND not product_template.type='service' AND (res_country.intrastat=true OR (inv.intrastat_country_id is NULL AND countrypartner.intrastat=true)) AND ((res_country.code IS NOT NULL AND not res_country.code=%s) OR (res_country.code is NULL AND countrypartner.code IS NOT NULL AND not countrypartner.code=%s)) AND inv.type IN (%s, %s) AND to_char(inv.date_invoice, 'YYYY')=%s AND to_char(inv.date_invoice, 'MM')=%s """ self.env.cr.execute(query, (company.id, company.partner_id.country_id.code, company.partner_id.country_id.code, mode1, mode2, self.year, self.month)) lines = self.env.cr.fetchall() invoicelines_ids = [rec[0] for rec in lines] invoicelines = self.env['account.invoice.line'].browse(invoicelines_ids) for inv_line in invoicelines: #Check type of transaction if inv_line.intrastat_transaction_id: extta = inv_line.intrastat_transaction_id.code else: extta = "1" #Check country if inv_line.invoice_id.intrastat_country_id: excnt = inv_line.invoice_id.intrastat_country_id.code else: excnt = inv_line.invoice_id.partner_id.country_id.code #Check region #If purchase, comes from purchase order, linked to a location, #which is linked to the warehouse #if sales, the sale order is linked to the warehouse #if sales, from a delivery order, linked to a location, #which is linked to the warehouse #If none found, get the company one. exreg = None if inv_line.invoice_id.type in ('in_invoice', 'in_refund'): #comes from purchase po_lines = self.env['purchase.order.line'].search([('invoice_lines', 'in', inv_line.id)], limit=1) if po_lines: if self._is_situation_triangular(company, po_line=po_lines): continue location = self.env['stock.location'].browse(po_lines.order_id._get_destination_location()) region_id = self.env['stock.warehouse'].get_regionid_from_locationid(location) if region_id: exreg = IntrastatRegion.browse(region_id).code elif inv_line.invoice_id.type in ('out_invoice', 'out_refund'): #comes from sales so_lines = self.env['sale.order.line'].search([('invoice_lines', 'in', inv_line.id)], limit=1) if so_lines: if self._is_situation_triangular(company, so_line=so_lines): continue saleorder = so_lines.order_id if saleorder and saleorder.warehouse_id and saleorder.warehouse_id.region_id: exreg = IntrastatRegion.browse(saleorder.warehouse_id.region_id.id).code if not exreg: if company.region_id: exreg = company.region_id.code else: self._company_warning(_('The Intrastat Region of the selected company is not set, ' 'please make sure to configure it first.')) #Check commodity codes intrastat_id = inv_line.product_id.get_intrastat_recursively() if intrastat_id: exgo = self.env['report.intrastat.code'].browse(intrastat_id).name else: raise exceptions.Warning( _('Product "%s" has no intrastat code, please configure it') % inv_line.product_id.display_name) #In extended mode, 2 more fields required if extendedmode: #Check means of transport if inv_line.invoice_id.transport_mode_id: extpc = inv_line.invoice_id.transport_mode_id.code elif company.transport_mode_id: extpc = company.transport_mode_id.code else: self._company_warning(_('The default Intrastat transport mode of your company ' 'is not set, please make sure to configure it first.')) #Check incoterm if inv_line.invoice_id.incoterm_id: exdeltrm = inv_line.invoice_id.incoterm_id.code elif company.incoterm_id: exdeltrm = company.incoterm_id.code else: self._company_warning(_('The default Incoterm of your company is not set, ' 'please make sure to configure it first.')) else: extpc = "" exdeltrm = "" linekey = intrastatkey(EXTRF=declcode, EXCNT=excnt, EXTTA=extta, EXREG=exreg, EXGO=exgo, EXTPC=extpc, EXDELTRM=exdeltrm) #We have the key #calculate amounts if inv_line.price_unit and inv_line.quantity: amount = inv_line.price_unit * inv_line.quantity else: amount = 0 weight = (inv_line.product_id.weight or 0.0) * \ inv_line.uom_id._compute_quantity(inv_line.quantity, inv_line.product_id.uom_id) if not inv_line.product_id.uom_id.category_id: supply_units = inv_line.quantity else: supply_units = inv_line.quantity * inv_line.uom_id.factor amounts = entries.setdefault(linekey, (0, 0, 0)) amounts = (amounts[0] + amount, amounts[1] + weight, amounts[2] + supply_units) entries[linekey] = amounts numlgn = 0 for linekey in entries: amounts = entries[linekey] if round(amounts[0], 0) == 0: continue numlgn += 1 item = ET.SubElement(datas, 'Item') self._set_Dim(item, 'EXSEQCODE', unicode(numlgn)) self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF)) self._set_Dim(item, 'EXCNT', unicode(linekey.EXCNT)) self._set_Dim(item, 'EXTTA', unicode(linekey.EXTTA)) self._set_Dim(item, 'EXREG', unicode(linekey.EXREG)) self._set_Dim(item, 'EXTGO', unicode(linekey.EXGO)) if extendedmode: self._set_Dim(item, 'EXTPC', unicode(linekey.EXTPC)) self._set_Dim(item, 'EXDELTRM', unicode(linekey.EXDELTRM)) self._set_Dim(item, 'EXTXVAL', unicode(round(amounts[0], 0)).replace(".", ",")) self._set_Dim(item, 'EXWEIGHT', unicode(round(amounts[1], 0)).replace(".", ",")) self._set_Dim(item, 'EXUNITS', unicode(round(amounts[2], 0)).replace(".", ",")) if numlgn == 0: #no datas datas.set('action', 'nihil') return decl def _set_Dim(self, item, prop, value): dim = ET.SubElement(item, 'Dim') dim.set('prop', prop) dim.text = value def _is_situation_triangular(self, company, po_line=False, so_line=False): # Ignoring what is purchased and sold by us with a dropshipping route # outside of our country, or completely within it # https://www.nbb.be/doc/dq/f_pdf_ex/intra2017fr.pdf (ยง 4.x) dropship_pick_type = self.env.ref('stock_dropshipping.picking_type_dropship', raise_if_not_found=False) if not dropship_pick_type: return False stock_move_domain = [('picking_type_id', '=', dropship_pick_type.id)] if po_line: stock_move_domain.append(('purchase_line_id', '=', po_line.id)) if so_line: stock_move_domain.append(('procurement_id.sale_line_id', '=', so_line.id)) stock_move = self.env['stock.move'].search(stock_move_domain, limit=1) return stock_move and ( (stock_move.partner_id.country_id.code != company.country_id.code and stock_move.picking_partner_id.country_id.code != company.country_id.code) or (stock_move.partner_id.country_id.code == company.country_id.code and stock_move.picking_partner_id.country_id.code == company.country_id.code))