odoo/addons/l10n_be_intrastat/wizard/xml_decl.py

315 lines
15 KiB
Python
Raw Normal View History

# -*- 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))