init
16
hide_send_message_button/__manifest__.py
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
'name': 'Hide Send Message Button',
|
||||
'version': '1.0',
|
||||
'category': 'Hidden',
|
||||
'summary': 'Nasconde il pulsante Invia messaggio nel chatter',
|
||||
'depends': ['mail'],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hide_send_message_button/static/src/css/hide_send_button.css',
|
||||
'hide_send_message_button/static/src/js/hide_send_button.js',
|
||||
# 'web/static/src/js/core/dom_ready.js', # <-- aggiungi questa riga
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
.o-mail-Chatter-sendMessage {
|
||||
display: none !important;
|
||||
}
|
||||
36
hide_send_message_button/static/src/js/hide_send_button.js
Normal file
@ -0,0 +1,36 @@
|
||||
odoo.define('hide_send_message_button.hide_send_button', [], function () {
|
||||
"use strict";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function hideButton() {
|
||||
const btn = document.querySelector('.o-mail-Chatter-sendMessage');
|
||||
if (btn) {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
hideButton();
|
||||
|
||||
const observer = new MutationObserver(hideButton);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// odoo.define('hide_send_message_button.hide_send_button', function (require) {
|
||||
// "use strict";
|
||||
|
||||
// const { patch } = require('web.utils');
|
||||
// const Chatter = require('mail.Chatter');
|
||||
|
||||
// patch(Chatter.prototype, {
|
||||
// mounted() {
|
||||
// this._super(...arguments);
|
||||
// const btn = this.el.querySelector('.o-mail-Chatter-sendMessage');
|
||||
// if (btn) {
|
||||
// btn.style.display = 'none';
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
8
hide_send_message_button/views/assets.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/hide_send_message_button/static/src/css/hide_send_button.css"/>
|
||||
<script type="module" src="/hide_send_message_button/static/src/js/hide_send_button.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
2
morpheus_contacts/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
24
morpheus_contacts/__manifest__.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Partner Info', # Se il tuo modulo è 'morpheus_contacts', sostituisci qui
|
||||
'version': '1.0',
|
||||
'summary': 'Aggiunge campi nel "FORM Contatto" e nella scheda contatti.',
|
||||
'description': """
|
||||
Questo modulo estende il form del contatto (res.partner) per includere
|
||||
una nuova scheda "FORM Contatto" con altri campi.
|
||||
""",
|
||||
'author': 'Il Tuo Nome/Azienda',
|
||||
'website': 'Il Tuo Sito Web (opzionale)',
|
||||
'category': 'Customer Relationship Management',
|
||||
'depends': ['base'],
|
||||
'data': [
|
||||
'views/res_partner_view.xml',
|
||||
'data/settori.xml',
|
||||
'data/fornitore_attuale_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/tag_contatto.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
BIN
morpheus_contacts/__pycache__/__init__.cpython-312.pyc
Normal file
13
morpheus_contacts/data/fornitore_attuale_data.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<record id="fornitore_option_marchi_diretti" model="fornitore.attuale.option">
|
||||
<field name="name">Marchi Diretti</field>
|
||||
</record>
|
||||
<record id="fornitore_option_grossista" model="fornitore.attuale.option">
|
||||
<field name="name">Grossista - Distributore</field>
|
||||
</record>
|
||||
<record id="fornitore_option_dettaglianti" model="fornitore.attuale.option">
|
||||
<field name="name">Dettaglianti</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
16
morpheus_contacts/data/settori.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<record id="settore_abbigliamento" model="res.partner.sector">
|
||||
<field name="name">Abbigliamento - Borse - Scarpe</field>
|
||||
</record>
|
||||
<record id="settore_cosmetici" model="res.partner.sector">
|
||||
<field name="name">Cosmetici & Profumi</field>
|
||||
</record>
|
||||
<record id="settore_occhialeria" model="res.partner.sector">
|
||||
<field name="name">Occhialeria</field>
|
||||
</record>
|
||||
<record id="settore_altre_categorie" model="res.partner.sector">
|
||||
<field name="name">Altre Categorie</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
morpheus_contacts/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import res_partner
|
||||
BIN
morpheus_contacts/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
morpheus_contacts/models/__pycache__/res_partner.cpython-312.pyc
Normal file
85
morpheus_contacts/models/res_partner.py
Normal file
@ -0,0 +1,85 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
is_favorite_contact = fields.Boolean(
|
||||
string="Contatto Preferito",
|
||||
help="Spunta questa casella se è un contatto prioritario per questa azienda."
|
||||
)
|
||||
|
||||
percent_provvigioni = fields.Float(
|
||||
string="% Provvigioni",
|
||||
digits=(5, 2),
|
||||
help="Inserisci la percentuale"
|
||||
)
|
||||
|
||||
cliente_fornitore_tipo = fields.Selection([
|
||||
('cliente', 'Cliente'),
|
||||
('fornitore', 'Fornitore'),
|
||||
('entrambi', 'Entrambi')
|
||||
], string='Cliente/Fornitore')
|
||||
|
||||
|
||||
agente_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Agente",
|
||||
domain="[('is_company', '=', False)]", # Filtra per i contatti che sono persone
|
||||
help="Seleziona l'agente di riferimento"
|
||||
)
|
||||
|
||||
|
||||
ceo_name = fields.Char(string="CEO Name")
|
||||
data_inizio_attivita = fields.Date(string="Data inizio attività")
|
||||
|
||||
fatturato_annuo = fields.Monetary(string="Fatturato annuo / Ricavi", currency_field='currency_id')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
string='Valuta',
|
||||
default=lambda self: self.env.company.currency_id.id,
|
||||
)
|
||||
|
||||
# Sezione "FORM Contatto"
|
||||
settori = fields.Many2many('res.partner.sector', string="Settori")
|
||||
|
||||
origine_contatto = fields.Selection([
|
||||
('motore_ricerca', 'Motore di ricerca'),
|
||||
('linkedin_social', 'LinkedIn / Altri social media'),
|
||||
('stampa_media', 'Financial Times / Altri media'),
|
||||
('passaparola', 'Passaparola'),
|
||||
('email', 'E-mail'),
|
||||
('sitointernet', 'Sito Internet'),
|
||||
('organico', 'Organico')
|
||||
], string="Origine Contatto")
|
||||
|
||||
identita_cliente = fields.Selection([
|
||||
('grossista_commerciante', 'Grossista - Commerciante'),
|
||||
('distributore', 'Distributore'),
|
||||
('e_commerce', 'E-commerce'),
|
||||
('catena_negozi', 'Catena di negozi'),
|
||||
('centro_commerciale_outlet', 'Centro Commerciale - Outlet'),
|
||||
('agente', 'Agente'),
|
||||
], string="Identità Cliente")
|
||||
|
||||
acquisti_da_italia = fields.Selection([
|
||||
('si', 'Sì'),
|
||||
('no', 'No')
|
||||
], string="State acquistando dall'Italia?")
|
||||
|
||||
|
||||
fornitori_attuali_ids = fields.Many2many(
|
||||
'fornitore.attuale.option',
|
||||
string="Fornitori Attuali dei Clienti"
|
||||
)
|
||||
|
||||
class ResPartnerSector(models.Model):
|
||||
_name = 'res.partner.sector' # Nome tecnico corretto
|
||||
_description = 'Settore Partner'
|
||||
|
||||
name = fields.Char(string="Nome Settore")
|
||||
|
||||
class FornitoreAttualeOption(models.Model):
|
||||
_name = 'fornitore.attuale.option'
|
||||
_description = 'Opzioni Fornitore Attuale'
|
||||
|
||||
name = fields.Char(string="Tipo di Fornitore", required=True)
|
||||
3
morpheus_contacts/security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_res_partner_sector_user,res.partner.sector.user,model_res_partner_sector,base.group_user,1,1,1,1
|
||||
access_fornitore_attuale_option_user,Fornitore Attuale Option access,model_fornitore_attuale_option,base.group_user,1,0,0,0
|
||||
|
42
morpheus_contacts/views/res_partner_view.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<odoo>
|
||||
<record id="view_partner_form_custom_full" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.custom.full</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//page[@name='sales_purchases']//field[@name='user_id']" position="after">
|
||||
<field name="agente_id" context="{'default_is_company': False, 'default_company_type': 'person'}" string="Agente"/>
|
||||
<field name="percent_provvigioni"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Campi extra nella vista principale -->
|
||||
<xpath expr="//sheet/group" position="inside">
|
||||
<group string="Informazioni Aziendali">
|
||||
|
||||
<field name="cliente_fornitore_tipo"/>
|
||||
<field name="ceo_name"/>
|
||||
<field name="data_inizio_attivita"/>
|
||||
<field name="fatturato_annuo"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<!-- Nuova pagina nel notebook -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Customer Form">
|
||||
<group string="Informazioni Generali">
|
||||
<field name="settori" widget="many2many_checkboxes"/>
|
||||
<field name="origine_contatto"/>
|
||||
</group>
|
||||
<group string="Informazioni Merceologiche">
|
||||
<field name="identita_cliente"/>
|
||||
<field name="acquisti_da_italia"/>
|
||||
<field name="fornitori_attuali_ids" widget="many2many_checkboxes"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
52
morpheus_contacts/views/tag_contatto.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_partner_form_inherit_child_quick_create" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.child.quick.create</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="after">
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Etichette..."/>
|
||||
<field name="is_favorite_contact" widget="boolean_favorite"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_form_inherit_child_kanban_create" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.child.kanban.create</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='child_ids']" position="inside">
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<field name="title"/>
|
||||
<field name="email"/>
|
||||
<field name="category_id"/>
|
||||
<field name="is_favorite_contact" widget="boolean_favorite"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="oe_kanban_details">
|
||||
<strong><field name="name"/></strong>
|
||||
<div t-if="record.title.raw_value">
|
||||
<t t-esc="record.title.value"/>
|
||||
</div>
|
||||
<div t-if="record.email.raw_value" class="o_text_overflow">
|
||||
<t t-esc="record.email.value"/>
|
||||
</div>
|
||||
<field name="category_id" widget="many2many_tags"/>
|
||||
<field name="is_favorite_contact" widget="boolean_favorite"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
2
morpheus_crm/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
17
morpheus_crm/__manifest__.py
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
'name': 'Morpheus CRM',
|
||||
'version': '1.0',
|
||||
'category': 'CRM',
|
||||
'summary': 'Estensioni CRM con wishlist cliente',
|
||||
'depends': ['crm'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/crm_wishlist_view.xml',
|
||||
'data/crm_wishlist_categoria_data.xml',
|
||||
'data/crm_wishlist_sottocategoria_data.xml',
|
||||
'data/crm_lead_genere_data.xml',
|
||||
'views/crm_kanban_view.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
BIN
morpheus_crm/__pycache__/__init__.cpython-312.pyc
Normal file
18
morpheus_crm/data/crm_lead_genere_data.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<odoo>
|
||||
<data noupdate="1"> <record id="genere_uomo" model="crm.lead.genere">
|
||||
<field name="name">Uomo</field>
|
||||
</record>
|
||||
<record id="genere_donna" model="crm.lead.genere">
|
||||
<field name="name">Donna</field>
|
||||
</record>
|
||||
<record id="genere_bambino" model="crm.lead.genere">
|
||||
<field name="name">Bambino</field>
|
||||
</record>
|
||||
<record id="genere_pets" model="crm.lead.genere">
|
||||
<field name="name">Pets</field>
|
||||
</record>
|
||||
<record id="genere_altro" model="crm.lead.genere">
|
||||
<field name="name">Altro</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
25
morpheus_crm/data/crm_wishlist_categoria_data.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<record id="categoria_fashion" model="crm.wishlist.categoria">
|
||||
<field name="name">Fashion</field>
|
||||
</record>
|
||||
<record id="categoria_beauty" model="crm.wishlist.categoria">
|
||||
<field name="name">Beauty</field>
|
||||
</record>
|
||||
<record id="categoria_eyewear" model="crm.wishlist.categoria">
|
||||
<field name="name">Eyewear</field>
|
||||
</record>
|
||||
<record id="categoria_home" model="crm.wishlist.categoria">
|
||||
<field name="name">Home</field>
|
||||
</record>
|
||||
<record id="categoria_gift" model="crm.wishlist.categoria">
|
||||
<field name="name">Gift</field>
|
||||
</record>
|
||||
<record id="categoria_food_beverage" model="crm.wishlist.categoria">
|
||||
<field name="name">Food & Beverage</field>
|
||||
</record>
|
||||
<record id="categoria_other" model="crm.wishlist.categoria">
|
||||
<field name="name">Other</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
95
morpheus_crm/data/crm_wishlist_sottocategoria_data.xml
Normal file
@ -0,0 +1,95 @@
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- Fashion -->
|
||||
<record id="sottocat_fashion_scarpe" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Scarpe</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_fashion"/>
|
||||
</record>
|
||||
<record id="sottocat_fashion_abbigliamento" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Abbigliamento</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_fashion"/>
|
||||
</record>
|
||||
<record id="sottocat_fashion_borse" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Borse</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_fashion"/>
|
||||
</record>
|
||||
<record id="sottocat_fashion_accessori" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Accessori</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_fashion"/>
|
||||
</record>
|
||||
|
||||
<!-- Beauty -->
|
||||
<record id="sottocat_beauty_skincare" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Skincare</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_beauty"/>
|
||||
</record>
|
||||
<record id="sottocat_beauty_makeup" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Make-up</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_beauty"/>
|
||||
</record>
|
||||
<record id="sottocat_beauty_profumi" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Profumi</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_beauty"/>
|
||||
</record>
|
||||
|
||||
<!-- Eyewear -->
|
||||
<record id="sottocat_eyewear_occhiali_da_sole" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Occhiali da sole</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_eyewear"/>
|
||||
</record>
|
||||
<record id="sottocat_eyewear_occhiali_da_vista" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Occhiali da vista</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_eyewear"/>
|
||||
</record>
|
||||
|
||||
<!-- Home -->
|
||||
<record id="sottocat_home_arredamento" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Arredamento</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_home"/>
|
||||
</record>
|
||||
<record id="sottocat_home_decorazioni" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Decorazioni</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_home"/>
|
||||
</record>
|
||||
<record id="sottocat_home_illuminazione" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Illuminazione</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_home"/>
|
||||
</record>
|
||||
|
||||
<!-- Gift -->
|
||||
<record id="sottocat_gift_gadget" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Gadget</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_gift"/>
|
||||
</record>
|
||||
<record id="sottocat_gift_regali_aziendali" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Regali aziendali</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_gift"/>
|
||||
</record>
|
||||
<!-- <record id="sottocat_gift_confezioni" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Confezioni regalo</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_gift"/>
|
||||
</record> -->
|
||||
|
||||
<!-- Food & Beverage -->
|
||||
<record id="sottocat_food_beverage_vini" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Vini</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_food_beverage"/>
|
||||
</record>
|
||||
<record id="sottocat_food_beverage_dolci" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Dolci</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_food_beverage"/>
|
||||
</record>
|
||||
<record id="sottocat_food_beverage_prodotti_tipici" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Prodotti tipici</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_food_beverage"/>
|
||||
</record>
|
||||
|
||||
<!-- Other -->
|
||||
<record id="sottocat_other_varie" model="crm.wishlist.sottocategoria">
|
||||
<field name="name">Varie</field>
|
||||
<field name="categoria_id" ref="morpheus_crm.categoria_other"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
2
morpheus_crm/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import crm_lead
|
||||
BIN
morpheus_crm/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
morpheus_crm/models/__pycache__/crm_lead.cpython-312.pyc
Normal file
203
morpheus_crm/models/crm_lead.py
Normal file
@ -0,0 +1,203 @@
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class CrmLead(models.Model):
|
||||
_inherit = 'crm.lead'
|
||||
|
||||
addetto_acquisti = fields.Many2one(
|
||||
'res.users',
|
||||
string='Addetto acquisti'
|
||||
)
|
||||
|
||||
addetto_acquisti_2 = fields.Many2one(
|
||||
'res.users',
|
||||
string='Addetto acquisti 2'
|
||||
)
|
||||
|
||||
addetto_vendite_2 = fields.Many2one(
|
||||
'res.users',
|
||||
string='Addetto vendite 2'
|
||||
)
|
||||
|
||||
|
||||
def action_set_stage_wishlist_verificata(self):
|
||||
self.ensure_one() # Assicura che il metodo sia chiamato su un singolo record, tipico per un click da form.
|
||||
|
||||
# Trova lo stadio di destinazione "Wishlist verificata"
|
||||
# Per maggiore robustezza, se conosci l'XML ID dello stadio, usa:
|
||||
# target_stage = self.env.ref('nome_tuo_modulo.xml_id_dello_stadio_wishlist_verificata', raise_if_not_found=False)
|
||||
target_stage = self.env['crm.stage'].search([('name', '=', 'Wishlist verificata')], limit=1)
|
||||
|
||||
if not target_stage:
|
||||
raise UserError(_("Lo stadio 'Wishlist verificata' non è stato trovato. Assicurati che esista."))
|
||||
|
||||
# Verifica se l'opportunità è effettivamente nello stadio "Nuova" prima di cambiarlo
|
||||
# (questa è una doppia verifica, dato che l'XML dovrebbe già nascondere il bottone)
|
||||
if self.stage_id.name == 'Nuova':
|
||||
if self.stage_id.id == target_stage.id:
|
||||
# Già nello stadio desiderato (improbabile se il bottone è visibile solo in "Nuova")
|
||||
return True
|
||||
self.write({'stage_id': target_stage.id})
|
||||
else:
|
||||
# Questo messaggio potrebbe apparire se la condizione 'invisible' nell'XML non fosse perfetta
|
||||
# o se il metodo fosse chiamato da un'altra parte.
|
||||
raise UserError(_("L'opportunità deve essere nello stadio 'Nuova' per poterla inviare a 'Wishlist verificata'."))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
verifica_target = fields.Selection([
|
||||
('yes', 'Sì'),
|
||||
('no', 'No')
|
||||
], string="Verificato target ITA/EEUU")
|
||||
|
||||
motivo_verifica_negata = fields.Text(string="Motivo mancata verifica")
|
||||
|
||||
percent_provvigioni = fields.Float(
|
||||
string="% Provvigioni",
|
||||
digits=(5, 2),
|
||||
help="Inserisci la percentuale"
|
||||
)
|
||||
|
||||
|
||||
agente_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Agente",
|
||||
domain="[('is_company', '=', False)]",
|
||||
help="Seleziona l'agente di riferimento")
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id_set_agente(self):
|
||||
if self.partner_id:
|
||||
if self.partner_id.agente_id:
|
||||
self.agente_id = self.partner_id.agente_id
|
||||
self.percent_provvigioni = self.partner_id.percent_provvigioni
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('partner_id'):
|
||||
partner = self.env['res.partner'].browse(vals['partner_id'])
|
||||
if not vals.get('agente_id') and partner.agente_id:
|
||||
vals['agente_id'] = partner.agente_id.id
|
||||
if not vals.get('percent_provvigioni'):
|
||||
vals['percent_provvigioni'] = partner.percent_provvigioni
|
||||
return super(CrmLead, self).create(vals)
|
||||
|
||||
|
||||
# Campi per la Richiesta Offerta
|
||||
fornitore_id = fields.Many2one('res.partner', string="Fornitore")
|
||||
buyer = fields.Char(string="Buyer")
|
||||
prodotti_richiesti = fields.Text(string="Prodotti Richiesti")
|
||||
minimo_ordine = fields.Char(string="Minimo Ordine")
|
||||
|
||||
# Modifica del campo categoria_ids (Many2many)
|
||||
categoria_ids = fields.Many2many(
|
||||
'crm.wishlist.categoria',
|
||||
string="Categorie"
|
||||
)
|
||||
|
||||
sottocategoria_ids = fields.Many2many(
|
||||
'crm.wishlist.sottocategoria',
|
||||
string="Sottocategorie",
|
||||
domain="[('categoria_id', 'in', categoria_ids)]"
|
||||
)
|
||||
|
||||
stagione = fields.Selection([
|
||||
('close_out', 'Close Out'),
|
||||
('attuale', 'Attuale'),
|
||||
('preordine', 'Pre-ordine'),
|
||||
], string="Stagione")
|
||||
|
||||
budget = fields.Monetary(string="Budget", currency_field='currency_id')
|
||||
currency_id = fields.Many2one('res.currency', string='Valuta', default=lambda self: self.env.company.currency_id.id)
|
||||
|
||||
tipo_richiesta = fields.Selection([
|
||||
('quotazione', 'Quotazione'),
|
||||
('generica', 'Generica'),
|
||||
], string="Tipo Richiesta")
|
||||
|
||||
brand = fields.Char(string="Brand")
|
||||
target = fields.Char(string="Target")
|
||||
|
||||
genere_ids = fields.Many2many(
|
||||
'crm.lead.genere', # Riferimento al nuovo modello
|
||||
string="Genere"
|
||||
)
|
||||
|
||||
note = fields.Text(string="Note")
|
||||
|
||||
|
||||
shelf_life = fields.Char(string="Shelf life")
|
||||
allegato_ids = fields.Many2many('ir.attachment', string="Allegati")
|
||||
|
||||
# Computazione del completamento della wishlist
|
||||
wishlist_complete_percent = fields.Float(
|
||||
string="Completamento Wishlist (%)",
|
||||
compute="_compute_wishlist_complete_percent",
|
||||
store=True
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'categoria_ids',
|
||||
'sottocategoria_ids',
|
||||
'stagione',
|
||||
'budget',
|
||||
'tipo_richiesta',
|
||||
'brand',
|
||||
'target',
|
||||
'genere_ids',
|
||||
'verifica_target',
|
||||
)
|
||||
def _compute_wishlist_complete_percent(self):
|
||||
for lead in self:
|
||||
fields_to_check = [
|
||||
'categoria_ids', 'sottocategoria_ids', 'stagione', 'budget',
|
||||
'tipo_richiesta', 'brand', 'target',
|
||||
'genere_ids', 'verifica_target',
|
||||
]
|
||||
total = len(fields_to_check)
|
||||
completed = 0
|
||||
|
||||
for field_name in fields_to_check:
|
||||
value = getattr(lead, field_name)
|
||||
if value: # Per i Many2many, questo verifica se la relazione non è vuota
|
||||
completed += 1
|
||||
|
||||
lead.wishlist_complete_percent = round((completed / total) * 100.0, 2) if total else 0.0
|
||||
|
||||
@api.onchange('tipo_richiesta')
|
||||
def _onchange_tipo_richiesta(self):
|
||||
if self.tipo_richiesta == 'quotazione':
|
||||
self.priority = '3'
|
||||
elif self.tipo_richiesta == 'generica':
|
||||
self.priority = '2'
|
||||
else:
|
||||
self.priority = '1'
|
||||
|
||||
|
||||
class WishlistCategoria(models.Model):
|
||||
_name = 'crm.wishlist.categoria'
|
||||
_description = 'Categoria Wishlist'
|
||||
|
||||
name = fields.Char(string="Nome Categoria")
|
||||
|
||||
class WishlistSottocategoria(models.Model):
|
||||
_name = 'crm.wishlist.sottocategoria'
|
||||
_description = 'Sottocategoria Wishlist'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
categoria_id = fields.Many2one(
|
||||
'crm.wishlist.categoria',
|
||||
string='Categoria di appartenenza',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
|
||||
# Nuovo modello per le opzioni del Genere
|
||||
class CrmLeadGenere(models.Model):
|
||||
_name = 'crm.lead.genere'
|
||||
_description = 'Genere per Lead CRM'
|
||||
|
||||
name = fields.Char(string="Nome Genere", required=True, translate=True)
|
||||
6
morpheus_crm/security/ir.model.access.csv
Normal file
@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_crm_wishlist_categoria_user,access.crm.wishlist.categoria.user,model_crm_wishlist_categoria,base.group_user,1,1,1,1
|
||||
access_crm_wishlist_categoria_manager,access.crm.wishlist.categoria.manager,model_crm_wishlist_categoria,base.group_system,1,1,1,1
|
||||
access_crm_wishlist_sottocategoria_user,access.crm.wishlist.sottocategoria.user,model_crm_wishlist_sottocategoria,base.group_user,1,1,1,1
|
||||
access_crm_wishlist_sottocategoria_manager,access.crm.wishlist.sottocategoria.manager,model_crm_wishlist_sottocategoria,base.group_system,1,1,1,1
|
||||
access_crm_lead_genere_user,crm.lead.genere.user,model_crm_lead_genere,base.group_user,1,1,1,1
|
||||
|
18
morpheus_crm/views/crm_kanban_view.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<odoo>
|
||||
<record id="view_crm_lead_kanban_inherit_data_inserimento" model="ir.ui.view">
|
||||
<field name="name">crm.lead.kanban.inherit.data.inserimento</field>
|
||||
<field name="model">crm.lead</field>
|
||||
<field name="inherit_id" ref="crm.crm_case_kanban_view_leads"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Inserisce dopo il partner -->
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<div class="text-muted small">
|
||||
🕓 Data inserimento:
|
||||
<field name="create_date" readonly="1"/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
77
morpheus_crm/views/crm_wishlist_view.xml
Normal file
@ -0,0 +1,77 @@
|
||||
<odoo>
|
||||
<record id="view_crm_lead_form_custom_wishlist" model="ir.ui.view">
|
||||
<field name="name">crm.lead.form.wishlist</field>
|
||||
<field name="model">crm.lead</field>
|
||||
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//field[@name='user_id']" position="before">
|
||||
<field name="create_date" readonly="1"/>
|
||||
<field name="agente_id" context="{'default_is_company': False, 'default_company_type': 'person'}" string="Agente"/>
|
||||
<field name="percent_provvigioni"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="addetto_vendite_2"/>
|
||||
|
||||
<field name="addetto_acquisti"/>
|
||||
<field name="addetto_acquisti_2"/>
|
||||
</xpath>
|
||||
|
||||
<field name="day_open" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</field>
|
||||
|
||||
<field name="day_close" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</field>
|
||||
|
||||
<xpath expr="//sheet/notebook" position="inside">
|
||||
<page string="Wishlist Cliente">
|
||||
<group string="Completamento">
|
||||
<field name="wishlist_complete_percent" widget="progressbar"/>
|
||||
<button name="action_set_stage_wishlist_verificata"
|
||||
string="Invio Wishlist Verificata"
|
||||
type="object"
|
||||
help="Passa l'opportunità allo stadio Wishlist verificata"/>
|
||||
</group>
|
||||
<group string="Informazioni Wishlist" col="2">
|
||||
<field name="categoria_ids" widget="many2many_checkboxes"/>
|
||||
<field name="sottocategoria_ids" widget="many2many_checkboxes" domain="[('categoria_id', 'in', categoria_ids)]"/>
|
||||
<field name="stagione"/>
|
||||
<field name="budget"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="tipo_richiesta"/>
|
||||
</group>
|
||||
<group string="Dettagli Aggiuntivi" col="2">
|
||||
<field name="brand"/>
|
||||
<field name="target"/>
|
||||
<field name="verifica_target"/>
|
||||
<field name="motivo_verifica_negata" invisible="verifica_target != 'no'" required="verifica_target == 'no'" />
|
||||
<field name="genere_ids" widget="many2many_checkboxes"/>
|
||||
<field name="shelf_life"/>
|
||||
<field name="note"/>
|
||||
<field name="allegato_ids" widget="many2many_binary"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Richiesta Offerta">
|
||||
<group string="Informazioni Richiesta Offerta" col="2">
|
||||
<field name="fornitore_id"/>
|
||||
<field name="categoria_ids" widget="many2many_checkboxes"/>
|
||||
<field name="sottocategoria_ids" widget="many2many_checkboxes" domain="[('categoria_id', 'in', categoria_ids)]"/>
|
||||
<field name="stagione"/>
|
||||
<field name="buyer"/>
|
||||
<field name="prodotti_richiesti"/>
|
||||
</group>
|
||||
<group string="Dettagli Aggiuntivi" col="2">
|
||||
<field name="brand"/>
|
||||
<field name="minimo_ordine"/>
|
||||
<field name="genere_ids" widget="many2many_checkboxes"/>
|
||||
<field name="note"/>
|
||||
<field name="allegato_ids" widget="many2many_binary"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
morpheus_custom_pdf_report/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
26
morpheus_custom_pdf_report/__manifest__.py
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
'name': 'Custom PDF Report - ReportLab',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Genera PDF personalizzati da preventivi e ordini usando ReportLab',
|
||||
'description': """
|
||||
Modulo per creare PDF personalizzati partendo dalle informazioni
|
||||
di preventivi e ordini di vendita utilizzando la libreria ReportLab.
|
||||
|
||||
Compatibile con Odoo 18.0
|
||||
""",
|
||||
'author': 'Il Tuo Nome',
|
||||
'website': 'https://www.example.com',
|
||||
'depends': ['sale', 'base', 'mail'],
|
||||
'external_dependencies': {
|
||||
'python': ['reportlab'],
|
||||
},
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/letterhead_wizard_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
BIN
morpheus_custom_pdf_report/__pycache__/__init__.cpython-312.pyc
Normal file
1
morpheus_custom_pdf_report/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import sale_order
|
||||
897
morpheus_custom_pdf_report/models/sale_order.py
Normal file
@ -0,0 +1,897 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
import re
|
||||
|
||||
# Import ReportLab
|
||||
try:
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, Flowable, Indenter
|
||||
from reportlab.lib.units import inch, cm
|
||||
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
||||
from reportlab.lib.utils import ImageReader
|
||||
except ImportError:
|
||||
raise ImportError("ReportLab non è installato. Esegui: pip install reportlab")
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
|
||||
def _clean_html_text(self, html_text):
|
||||
if not html_text:
|
||||
return ""
|
||||
|
||||
text = html_text
|
||||
text = re.sub(r'<br\s*/?>', '\n', text)
|
||||
text = re.sub(r'<p[^>]*>', '\n\n', text)
|
||||
text = re.sub(r'</p>', '', text)
|
||||
text = re.sub(r'<div[^>]*>', '\n', text)
|
||||
text = re.sub(r'</div>', '', text)
|
||||
text = re.sub(r'<strong[^>]*>(.*?)</strong>', r'<b>\1</b>', text, flags=re.DOTALL)
|
||||
text = re.sub(r'<b[^>]*>(.*?)</b>', r'<b>\1</b>', text, flags=re.DOTALL)
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace(''', "'")
|
||||
|
||||
lines = text.split('\n')
|
||||
cleaned_lines = [' '.join(line.split()) for line in lines if line.strip()]
|
||||
result = '<br/>'.join(cleaned_lines)
|
||||
|
||||
return result
|
||||
|
||||
def _format_currency(self, amount):
|
||||
"""Formatta gli importi con separatore delle migliaia in formato italiano"""
|
||||
if not isinstance(amount, (int, float)):
|
||||
return ""
|
||||
formatted = f"{amount:,.2f}".replace(',', 'TEMP').replace('.', ',').replace('TEMP', '.')
|
||||
return f"€ {formatted}"
|
||||
|
||||
def action_generate_custom_pdf(self, letterhead_type=None, admin_fields=None):
|
||||
self.ensure_one()
|
||||
import os
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.platypus import SimpleDocTemplate, Image, Spacer, Table, TableStyle, Paragraph
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
import io
|
||||
import base64
|
||||
from datetime import datetime
|
||||
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buffer,
|
||||
pagesize=A4,
|
||||
rightMargin=1*cm,
|
||||
leftMargin=1*cm,
|
||||
topMargin=0.2*cm,
|
||||
bottomMargin=3*cm
|
||||
)
|
||||
|
||||
story = []
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
partner = self.partner_id
|
||||
|
||||
# Logo in alto a sinistra
|
||||
img = None
|
||||
module_path = os.path.dirname(os.path.abspath(__file__))
|
||||
logo_path = os.path.join(module_path, '../static/src/img/ap_logo_info.png')
|
||||
logo_path = os.path.normpath(logo_path)
|
||||
if os.path.exists(logo_path):
|
||||
img = Image(logo_path, width=300, height=170)
|
||||
img.hAlign = 'LEFT'
|
||||
# Dati cliente in alto a destra
|
||||
client_lines = []
|
||||
client_lines.append(f"<b>Customer Code No. </b> {partner.id}")
|
||||
if partner.name:
|
||||
client_lines.append(f"<b>{partner.name}</b>")
|
||||
if partner.street:
|
||||
client_lines.append(partner.street)
|
||||
if partner.street2:
|
||||
client_lines.append(partner.street2)
|
||||
city_line = ' '.join(filter(None, [partner.zip, partner.city]))
|
||||
if city_line:
|
||||
client_lines.append(city_line)
|
||||
if partner.country_id:
|
||||
client_lines.append(partner.country_id.name)
|
||||
client_style = ParagraphStyle('ClientStyle', parent=styles['Normal'], fontSize=14, leading=26, alignment=2)
|
||||
client_info = Paragraph('<br/>'.join(client_lines), client_style)
|
||||
# Tabella logo sx, cliente dx
|
||||
table_data = [[img if img else '', client_info]]
|
||||
table = Table(table_data, colWidths=[doc.width/2, doc.width/2])
|
||||
table.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('ALIGN', (0, 0), (0, 0), 'LEFT'),
|
||||
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
]))
|
||||
story.append(table)
|
||||
story.append(Spacer(1, 30))
|
||||
# Barra PROFORMA
|
||||
class ProformaHeader(Flowable):
|
||||
def __init__(self, width, proforma_text, page_text):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.proforma_text = proforma_text
|
||||
self.page_text = page_text
|
||||
self.height = 24
|
||||
def draw(self):
|
||||
self.canv.saveState()
|
||||
# Gradiente da nero (#000000) a fucsia (#f30ad0) come ThankYouBar
|
||||
steps = int(self.width * 2)
|
||||
rect_width = self.width / steps
|
||||
for i in range(steps):
|
||||
ratio = i / (steps - 1)
|
||||
r = int(0 + (243 - 0) * ratio)
|
||||
g = int(0 + (10 - 0) * ratio)
|
||||
b = int(0 + (208 - 0) * ratio)
|
||||
color = colors.Color(r/255, g/255, b/255)
|
||||
self.canv.setFillColor(color)
|
||||
self.canv.rect(i * rect_width, 0, rect_width + 0.5, self.height, stroke=0, fill=1)
|
||||
self.canv.setFont("Helvetica-Bold", 16)
|
||||
self.canv.setFillColor(colors.white)
|
||||
self.canv.drawCentredString(self.width/2, 6, self.proforma_text)
|
||||
self.canv.setFont("Helvetica", 10)
|
||||
self.canv.drawRightString(self.width-10, 6, self.page_text)
|
||||
self.canv.restoreState()
|
||||
def wrap(self, availWidth, availHeight):
|
||||
return self.width, self.height
|
||||
proforma_header = ProformaHeader(doc.width, "PROFORMA", "Pagina 1")
|
||||
story.append(proforma_header)
|
||||
story.append(Spacer(1, 20))
|
||||
# Colonna 3: PO Number e PO Date affiancati
|
||||
po_number_label = Paragraph('<b>PO Number</b>', ParagraphStyle('po_label', parent=styles['Normal'], alignment=2))
|
||||
po_number_value = Paragraph(self.name or '', ParagraphStyle('po_value', parent=styles['Normal'], alignment=2))
|
||||
po_date_label = Paragraph('<b>PO Date</b>', ParagraphStyle('po_label', parent=styles['Normal'], alignment=2))
|
||||
po_date_value = Paragraph(self.date_order.strftime('%d/%m/%Y') if self.date_order else '', ParagraphStyle('po_value', parent=styles['Normal'], alignment=2))
|
||||
col3_table = Table(
|
||||
[
|
||||
[po_number_label, po_date_label],
|
||||
[po_number_value, po_date_value]
|
||||
],
|
||||
colWidths=[(doc.width/3)/2, (doc.width/3)/2]
|
||||
)
|
||||
col3_table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 2),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 2),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOX', (0, 0), (-1, -1), 0, colors.white),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0, colors.white),
|
||||
]))
|
||||
col3 = col3_table
|
||||
|
||||
# Colonna 2: Fiscal Code sopra, sotto Currency e Payment Terms affiancati, con valori
|
||||
fiscal_code_label = Paragraph('<b>Fiscal Code</b>', ParagraphStyle('fiscal_label', parent=styles['Normal'], alignment=1))
|
||||
currency_label = Paragraph('<b>Currency</b>', ParagraphStyle('currency_label', parent=styles['Normal'], alignment=1))
|
||||
payment_terms_label = Paragraph('<b>Payment Terms</b>', ParagraphStyle('payment_label', parent=styles['Normal'], alignment=1))
|
||||
fiscal_code_value = Paragraph(getattr(partner, 'l10n_it_codice_fiscale', '') or '', ParagraphStyle('fiscal_value', parent=styles['Normal'], alignment=1))
|
||||
currency_value = Paragraph(self.currency_id.name or '', ParagraphStyle('currency_value', parent=styles['Normal'], alignment=1))
|
||||
payment_terms_value = Paragraph(self.payment_term_id.name or '', ParagraphStyle('payment_value', parent=styles['Normal'], alignment=1))
|
||||
col2_table = Table(
|
||||
[
|
||||
[fiscal_code_label, ''],
|
||||
[currency_label, payment_terms_label],
|
||||
[currency_value, payment_terms_value]
|
||||
],
|
||||
colWidths=[(doc.width/3)/2, (doc.width/3)/2]
|
||||
)
|
||||
col2_table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 2),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 2),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOX', (0, 0), (-1, -1), 0, colors.white),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0, colors.white),
|
||||
]))
|
||||
col2 = col2_table
|
||||
|
||||
# Colonna 1: Customer VAT Number, Customer Purchase Order, NOTES con valore (label allineata)
|
||||
vat_label = Paragraph('<b>Customer VAT Number</b>', ParagraphStyle('vat_label', parent=styles['Normal'], alignment=0))
|
||||
vat_value = Paragraph(partner.vat or '', ParagraphStyle('vat_value', parent=styles['Normal'], alignment=0))
|
||||
po_label = Paragraph('<b>Customer Purchase Order</b>', ParagraphStyle('po_label', parent=styles['Normal'], alignment=0))
|
||||
notes_label = Paragraph('<b>NOTES</b>', ParagraphStyle('notes_label', parent=styles['Normal'], alignment=0))
|
||||
notes_value = Paragraph(self.note or '', ParagraphStyle('notes_value', parent=styles['Normal'], alignment=0))
|
||||
col1 = [vat_label, vat_value, Spacer(1, 4), po_label, Spacer(1, 4), notes_label, notes_value]
|
||||
|
||||
second_row_table = Table(
|
||||
[[col1, col2, col3]],
|
||||
colWidths=[doc.width/3, doc.width/3, doc.width/3]
|
||||
)
|
||||
second_row_table.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (0, 0), 'TOP'),
|
||||
('VALIGN', (1, 0), (1, 0), 'TOP'),
|
||||
('VALIGN', (2, 0), (2, 0), 'TOP'),
|
||||
('ALIGN', (0, 0), (1, 0), 'LEFT'),
|
||||
('ALIGN', (2, 0), (2, 0), 'RIGHT'),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 8),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 8),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||
('BOX', (0, 0), (-1, -1), 0, colors.white),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0, colors.white),
|
||||
]))
|
||||
story.append(second_row_table)
|
||||
story.append(Spacer(1, 20))
|
||||
# Tabella delle righe del preventivo (8 colonne)
|
||||
header_style = ParagraphStyle('HeaderStyle', parent=styles['Normal'], textColor=colors.white, alignment=0, fontName='Helvetica-Bold', fontSize=7)
|
||||
order_line_headers = [
|
||||
Paragraph('<b>Code</b>', header_style),
|
||||
Paragraph('<b>Description</b>', header_style),
|
||||
Paragraph('<b>Pcs</b>', header_style),
|
||||
Paragraph('<b>Unit Price</b>', header_style),
|
||||
Paragraph('<b>Disc.%</b>', header_style),
|
||||
Paragraph('<b>Disc.%</b>', header_style),
|
||||
Paragraph('<b>Total</b>', header_style),
|
||||
Paragraph('<b>VAT Code</b>', header_style),
|
||||
]
|
||||
# Preparo le righe dati con wordwrap per le colonne di testo
|
||||
wrapped_order_line_rows = []
|
||||
for line in self.order_line:
|
||||
disc1 = getattr(line, 'discount', 0)
|
||||
disc2 = getattr(line, 'discount', 0)
|
||||
disc1_str = f"{disc1:.2f}" if disc1 != 0 else ''
|
||||
disc2_str = f"{disc2:.2f}" if disc2 != 0 else ''
|
||||
wrapped_order_line_rows.append([
|
||||
Paragraph(line.product_id.display_name or '', ParagraphStyle('wrap', parent=styles['Normal'], fontSize=9, wordWrap='CJK')),
|
||||
Paragraph(line.name or '', ParagraphStyle('wrap', parent=styles['Normal'], fontSize=9, wordWrap='CJK')),
|
||||
f"{line.product_uom_qty:.2f}",
|
||||
f"{line.price_unit:.2f}",
|
||||
disc1_str,
|
||||
disc2_str,
|
||||
f"{line.price_subtotal:.2f}",
|
||||
', '.join([tax.name for tax in line.tax_id]) if line.tax_id else '',
|
||||
])
|
||||
# Barra gradiente per header tabella dettaglio
|
||||
class DetailHeaderBar(Flowable):
|
||||
def __init__(self, width, height=24):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.height = height
|
||||
def draw(self):
|
||||
self.canv.saveState()
|
||||
steps = int(self.width * 2)
|
||||
rect_width = self.width / steps
|
||||
for i in range(steps):
|
||||
ratio = i / (steps - 1)
|
||||
r = int(0 + (243 - 0) * ratio)
|
||||
g = int(0 + (10 - 0) * ratio)
|
||||
b = int(0 + (208 - 0) * ratio)
|
||||
color = colors.Color(r/255, g/255, b/255)
|
||||
self.canv.setFillColor(color)
|
||||
self.canv.rect(i * rect_width, 0, rect_width + 0.5, self.height, stroke=0, fill=1)
|
||||
self.canv.restoreState()
|
||||
def wrap(self, availWidth, availHeight):
|
||||
return self.width, self.height
|
||||
# RIMUOVO la barra gradiente tra la tabella proforma e la tabella dei dettagli
|
||||
# story.append(DetailHeaderBar(doc.width, 24))
|
||||
# Aggiungo la riga delle intestazioni sopra la barra gradiente
|
||||
header_table = Table([
|
||||
order_line_headers
|
||||
], colWidths=[
|
||||
doc.width * 0.17, # Code
|
||||
doc.width * 0.38, # Description
|
||||
doc.width * 0.06, # Pcs
|
||||
doc.width * 0.10, # Unit Price
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.15, # Total (ingrandita)
|
||||
doc.width * 0.08, # VAT Code
|
||||
])
|
||||
header_table.setStyle(TableStyle([
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (1, 0), 'LEFT'), # Code, Description
|
||||
('ALIGN', (2, 0), (7, 0), 'RIGHT'), # tutte le altre colonne
|
||||
('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 20),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||||
('TOPPADDING', (0, 0), (-1, 0), 6),
|
||||
('LEFTPADDING', (0, 0), (-1, 0), 4),
|
||||
('RIGHTPADDING', (0, 0), (-1, 0), 4),
|
||||
]))
|
||||
story.append(header_table)
|
||||
def make_gradient_colors(start_hex, end_hex, n):
|
||||
from reportlab.lib.colors import HexColor, Color
|
||||
s = HexColor(start_hex)
|
||||
e = HexColor(end_hex)
|
||||
colors_list = []
|
||||
for i in range(n):
|
||||
ratio = i / max(n-1, 1)
|
||||
r = s.red + (e.red - s.red) * ratio
|
||||
g = s.green + (e.green - s.green) * ratio
|
||||
b = s.blue + (e.blue - s.blue) * ratio
|
||||
colors_list.append(Color(r, g, b))
|
||||
return colors_list
|
||||
# Tabella delle righe del preventivo (8 colonne) con header gradiente
|
||||
order_table_data = [order_line_headers] + wrapped_order_line_rows
|
||||
order_table = Table(order_table_data, colWidths=[
|
||||
doc.width * 0.17, # Code
|
||||
doc.width * 0.38, # Description
|
||||
doc.width * 0.06, # Pcs
|
||||
doc.width * 0.10, # Unit Price
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.10, # Total (ingrandita)
|
||||
doc.width * 0.08, # VAT Code
|
||||
])
|
||||
gradient_colors = make_gradient_colors('#000000', '#f30ad0', 8)
|
||||
order_table_style = TableStyle([
|
||||
*[
|
||||
('BACKGROUND', (col, 0), (col, 0), color)
|
||||
for col, color in enumerate(gradient_colors)
|
||||
],
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (1, -1), 'LEFT'), # Code, Description
|
||||
('ALIGN', (2, 0), (7, -1), 'RIGHT'), # tutte le altre colonne
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 20), # intestazione a 20pt
|
||||
('FONTSIZE', (0, 1), (-1, -1), 9), # righe dati a 10pt (ingrandito)
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||||
('TOPPADDING', (0, 0), (-1, 0), 6),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 4),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 4),
|
||||
# Nascondo completamente i bordi tra le colonne nella riga header
|
||||
('LINEBELOW', (0, 0), (-1, 0), 0, colors.transparent),
|
||||
('LINEABOVE', (0, 0), (-1, 0), 0, colors.transparent),
|
||||
('LINEBEFORE', (0, 0), (0, 0), 0, colors.transparent),
|
||||
('LINEAFTER', (-1, 0), (-1, 0), 0, colors.transparent),
|
||||
('INNERGRID', (0, 0), (-1, 0), 0, colors.transparent),
|
||||
('BOX', (0, 0), (-1, 0), 0, colors.transparent),
|
||||
('GRID', (0, 0), (-1, 0), 0, colors.transparent),
|
||||
# Linee orizzontali tra le righe dati
|
||||
('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.HexColor('#cccccc')), # sotto ogni riga dati
|
||||
('WORDWRAP', (0, 0), (-1, -1), 'CJK'), # Abilita il word wrap su tutte le celle
|
||||
])
|
||||
order_table.setStyle(order_table_style)
|
||||
story.append(order_table)
|
||||
story.append(Spacer(1, 10)) # Interlinea tra tabella di dettaglio e tabella sottostante
|
||||
# Replica della barra di intestazione sotto la tabella di dettaglio
|
||||
gradient_colors = make_gradient_colors('#000000', '#f30ad0', 8)
|
||||
header_bar_table = Table([
|
||||
order_line_headers
|
||||
], colWidths=[
|
||||
doc.width * 0.17, # Code
|
||||
doc.width * 0.38, # Description
|
||||
doc.width * 0.06, # Pcs
|
||||
doc.width * 0.10, # Unit Price
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.10, # Total (ingrandita)
|
||||
doc.width * 0.08, # VAT Code
|
||||
])
|
||||
header_bar_table.setStyle(TableStyle([
|
||||
*[
|
||||
('BACKGROUND', (col, 0), (col, 0), color)
|
||||
for col, color in enumerate(gradient_colors)
|
||||
],
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (1, 0), 'LEFT'), # Code, Description
|
||||
('ALIGN', (2, 0), (7, 0), 'RIGHT'), # tutte le altre colonne
|
||||
('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 20),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||||
('TOPPADDING', (0, 0), (-1, 0), 6),
|
||||
('LEFTPADDING', (0, 0), (-1, 0), 4),
|
||||
('RIGHTPADDING', (0, 0), (-1, 0), 4),
|
||||
]))
|
||||
story.append(header_bar_table)
|
||||
# Tabella Totali sotto e a destra della barra header
|
||||
# Calcolo larghezza delle ultime due colonne
|
||||
col_widths = [
|
||||
doc.width * 0.17, # Code
|
||||
doc.width * 0.38, # Description
|
||||
doc.width * 0.06, # Pcs
|
||||
doc.width * 0.10, # Unit Price
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.06, # Disc.%
|
||||
doc.width * 0.10, # Total (ingrandita)
|
||||
doc.width * 0.08, # VAT Code
|
||||
]
|
||||
# Calcolo i valori richiesti
|
||||
imponibile = sum([line.price_subtotal for line in self.order_line])
|
||||
total_vat = sum([(line.price_total - line.price_subtotal) for line in self.order_line])
|
||||
currency = self.currency_id and self.currency_id.name or ''
|
||||
# Definisco ora le label e i valori per la tabella Totali (senza le tre label iniziali)
|
||||
totals_labels = [
|
||||
'Total Taxable Vat',
|
||||
'Total Vat',
|
||||
f'Total Value Goods <b>{self.currency_id.name}</b>',
|
||||
'Deposit',
|
||||
'Additional Discount',
|
||||
'Additional Discount',
|
||||
'Total Fees',
|
||||
'<b>TOTAL PROFORMA</b>',
|
||||
]
|
||||
values_col = [
|
||||
f"{imponibile:,.2f}".replace(",", "TEMP").replace(".", ",").replace("TEMP", "."), # Total Taxable Vat senza valuta
|
||||
f"{total_vat:,.2f}".replace(",", "TEMP").replace(".", ",").replace("TEMP", "."), # Total Vat
|
||||
f"{imponibile:,.2f}".replace(",", "TEMP").replace(".", ",").replace("TEMP", "."), # Total Value Goods solo numero
|
||||
'', # Deposit
|
||||
'', # Additional Discount
|
||||
'', # Additional Discount
|
||||
'', # Total Fees
|
||||
'', # TOTAL PROFORMA (placeholder, verrà aggiornato dopo)
|
||||
]
|
||||
try:
|
||||
additional_discount_1 = float(values_col[4]) if values_col[4] else 0.0
|
||||
except Exception:
|
||||
additional_discount_1 = 0.0
|
||||
try:
|
||||
additional_discount_2 = float(values_col[5]) if values_col[5] else 0.0
|
||||
except Exception:
|
||||
additional_discount_2 = 0.0
|
||||
proforma_total = imponibile + total_vat - (additional_discount_1 + additional_discount_2)
|
||||
values_col[-1] = f"{proforma_total:,.2f}".replace(",", "TEMP").replace(".", ",").replace("TEMP", ".")
|
||||
# Larghezza identica alla barra header
|
||||
header_bar_width = sum(col_widths)
|
||||
shrink_factor = 0.32 # 32% della larghezza originale
|
||||
new_table_width = header_bar_width * shrink_factor
|
||||
label_col_width = new_table_width * 0.7
|
||||
value_col_width = new_table_width * 0.3
|
||||
indent = doc.width - new_table_width - 11
|
||||
# Ricostruisco totals_rows con la colonna valori
|
||||
new_totals_rows = []
|
||||
for i, label in enumerate(totals_labels):
|
||||
# Grassetto per Total Taxable Vat e Total Value Goods
|
||||
is_bold = 'Total Taxable Vat' in label or 'Total Value Goods' in label or 'TOTAL PROFORMA' in label
|
||||
style_label = ParagraphStyle('totals_label', parent=styles['Normal'], fontSize=10, fontName='Helvetica-Bold' if 'TOTAL PROFORMA' in label else 'Helvetica')
|
||||
style_value = ParagraphStyle('totals_value', parent=styles['Normal'], fontSize=10, alignment=2, fontName='Helvetica-Bold' if is_bold else 'Helvetica')
|
||||
new_totals_rows.append([
|
||||
Paragraph(label, style_label),
|
||||
Paragraph(values_col[i], style_value) if values_col[i] else ''
|
||||
])
|
||||
# Adatto la tabella Totali alla larghezza della colonna 3 della tabella esterna
|
||||
col1_width = doc.width - new_table_width - 11 - 5
|
||||
col2_width = 0
|
||||
col3_width = new_table_width
|
||||
|
||||
label_col_width = col3_width * 0.7
|
||||
value_col_width = col3_width * 0.3
|
||||
|
||||
class TotalsHeaderBar(Flowable):
|
||||
def __init__(self, width, height=24, text="Totale"):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.text = text
|
||||
def draw(self):
|
||||
self.canv.saveState()
|
||||
steps = int(self.width * 2)
|
||||
rect_width = self.width / steps
|
||||
for i in range(steps):
|
||||
ratio = i / (steps - 1)
|
||||
r = int(0 + (243 - 0) * ratio)
|
||||
g = int(0 + (10 - 0) * ratio)
|
||||
b = int(0 + (208 - 0) * ratio)
|
||||
color = colors.Color(r/255, g/255, b/255)
|
||||
self.canv.setFillColor(color)
|
||||
self.canv.rect(i * rect_width, 0, rect_width + 0.5, self.height, stroke=0, fill=1)
|
||||
self.canv.setFont("Helvetica-Bold", 14)
|
||||
self.canv.setFillColor(colors.white)
|
||||
self.canv.drawCentredString(self.width/2, 6, self.text)
|
||||
self.canv.restoreState()
|
||||
def wrap(self, availWidth, availHeight):
|
||||
return self.width, self.height
|
||||
# Uso la barra gradiente come header della tabella dei totali
|
||||
totals_table = Table(
|
||||
[[TotalsHeaderBar(new_table_width, 24), '']] + new_totals_rows,
|
||||
colWidths=[label_col_width, value_col_width]
|
||||
)
|
||||
totals_table.setStyle(TableStyle([
|
||||
('SPAN', (0, 0), (1, 0)),
|
||||
# Rimuovo il background fucsia, la barra gradiente è ora il Flowable TotalsHeaderBar
|
||||
#('BACKGROUND', (0, 0), (1, 0), colors.HexColor('#f30ad0')),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.lightgrey), # sfondo grigio a tutta la tabella dei totali
|
||||
('TEXTCOLOR', (0, 0), (1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (1, 0), 'CENTER'),
|
||||
('VALIGN', (0, 0), (1, 0), 'MIDDLE'),
|
||||
('FONTNAME', (0, 0), (1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (1, 0), 14),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10), # Applica 10pt a tutte le righe tranne header
|
||||
('BOTTOMPADDING', (0, 0), (1, 0), 0), # padding a 0 per la barra
|
||||
('TOPPADDING', (0, 0), (1, 0), 0),
|
||||
('LEFTPADDING', (0, 0), (1, 0), 0),
|
||||
('RIGHTPADDING', (0, 0), (1, 0), 0),
|
||||
('LINEABOVE', (0, 0), (1, 0), 0.5, colors.HexColor('#f30ad0')),
|
||||
('ALIGN', (0, 1), (0, -1), 'LEFT'),
|
||||
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
||||
('FONTNAME', (0, 8), (0, 8), 'Helvetica-Bold'),
|
||||
#('BOX', (0, 0), (-1, -1), 1, colors.black),
|
||||
#('INNERGRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
]))
|
||||
|
||||
# Recupero i valori richiesti per la mini-tabella
|
||||
# Total Taxable Vat dalla tabella Totali
|
||||
total_taxable_vat_value = values_col[0]
|
||||
# Codici VAT dalla colonna 7 della tabella dettaglio (solo uno, non ripetuto)
|
||||
vat_codes_list = [row[7] for row in wrapped_order_line_rows if row[7]] if wrapped_order_line_rows else []
|
||||
vat_codes_unique = ', '.join(sorted(set(vat_codes_list)))
|
||||
|
||||
# Creo la mini-tabella 2x3
|
||||
labels_row = ['Taxable Vat', 'VAT Code', 'VAT']
|
||||
values_row = [total_taxable_vat_value, vat_codes_unique, '']
|
||||
|
||||
# Nuovo blocco verticale per la colonna 1, riga 2
|
||||
|
||||
|
||||
# Ricostruisco la mini-tabella con la nuova colonna
|
||||
labels_table = Table([
|
||||
labels_row,
|
||||
values_row,
|
||||
# La terza riga con il valore duplicato è stata rimossa
|
||||
], colWidths=[col1_width*0.18, col1_width*0.18, col1_width*0.18])
|
||||
labels_table.setStyle(TableStyle([
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 10),
|
||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
||||
('ALIGN', (0, 1), (2, 1), 'CENTER'),
|
||||
('FONTNAME', (0, 1), (2, 1), 'Helvetica'),
|
||||
('FONTSIZE', (0, 1), (2, 1), 10),
|
||||
]))
|
||||
|
||||
# Creo una nuova tabella a 3 colonne: la prima riga contiene le label richieste, la seconda e terza sono vuote, la quarta contiene la tabella Totali con rowspan=3
|
||||
# Modifica: aggiungo le label richieste nella seconda riga, prima colonna
|
||||
# Ora la tabella ha due colonne: label e valore (solo per Total Goods)
|
||||
courier_labels = Table([
|
||||
[Paragraph('Total Goods', styles['Normal']), Paragraph(f'<b>{total_taxable_vat_value}</b>', ParagraphStyle('bold_total', parent=styles['Normal'], fontName='Helvetica-Bold', fontSize=7))],
|
||||
[Paragraph('Stamp Duty', styles['Normal']), ''],
|
||||
[Paragraph('Total Pieces', styles['Normal']), ''],
|
||||
], colWidths=[col1_width*0.27, col1_width*0.27])
|
||||
courier_labels.setStyle(TableStyle([
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica'),
|
||||
('FONTNAME', (1, 0), (1, 0), 'Helvetica-Bold'), # grassetto solo per il valore imponibile
|
||||
('FONTSIZE', (0, 0), (-1, -1), 7),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
('LEFTPADDING', (0, 0), (1, -1), 30),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
||||
('BACKGROUND', (0, 0), (0, 0), colors.lightgrey), # sfondo grigio alla cella colonna 1 riga 1
|
||||
('BACKGROUND', (1, 0), (1, 0), colors.lightgrey), # sfondo grigio all'importo accanto a Total Goods
|
||||
('BACKGROUND', (0, 1), (1, 1), colors.lightgrey), # sfondo grigio a Stamp Duty e valore
|
||||
('BACKGROUND', (0, 2), (1, 2), colors.lightgrey), # sfondo grigio a Total Pieces e valore
|
||||
]))
|
||||
outer_table = Table([
|
||||
[labels_table, '', totals_table],
|
||||
[courier_labels, '', ''],
|
||||
], colWidths=[col1_width, col2_width, col3_width])
|
||||
outer_table.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
#('BOX', (0, 0), (-1, -1), 1, colors.black),
|
||||
#('INNERGRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
('SPAN', (2, 0), (2, 1)), # rowspan della colonna 3 sulle 2 righe
|
||||
]))
|
||||
story.append(outer_table)
|
||||
story.append(Indenter(0, 0))
|
||||
story.append(Spacer(1, 10))
|
||||
# Label Bank Details in grassetto
|
||||
bank_details_label = Paragraph('<b>Bank Details</b>', styles['Normal'])
|
||||
story.append(bank_details_label)
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
# Nuova tabella 4 colonne, 1 riga, stessa larghezza della outer_table
|
||||
# Calcolo larghezza totale della tabella superiore
|
||||
bottom_table_total_width = col1_width + col2_width + col3_width
|
||||
bottom_table_col_width = bottom_table_total_width / 4
|
||||
bottom_table_col_widths = [bottom_table_col_width] * 4
|
||||
bottom_table = Table([
|
||||
['Account Name', 'Bank Name', 'BIC/SWIFT', 'IBAN']
|
||||
], colWidths=bottom_table_col_widths)
|
||||
bottom_table.setStyle(TableStyle([
|
||||
#('BOX', (0, 0), (-1, -1), 1, colors.black),
|
||||
#('INNERGRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), # sfondo grigio a tutta la riga
|
||||
]))
|
||||
small_footer_style = ParagraphStyle('small_footer', parent=styles['Normal'], fontSize=7)
|
||||
remarks_text = (
|
||||
'REMARKS :<br/>'
|
||||
'1. Bank commissions and currency exchange fees are at customer’s charge.<br/>'
|
||||
'2. Payments have to be processed in Euro currency.<br/>'
|
||||
'3. Deposit and Balance payments must be processed as per net amounts confirmed in the Proforma Invoice or Invoice. In case of short amount for the deposit, the remaining amount will be charged in the balance payment.<br/>'
|
||||
'Balance payments with short amounts will not be accepted.<br/>'
|
||||
'<br/>' # Interlinea sopra la scritta MANDATORY CLAIM SUBMISSION
|
||||
'<b>MANDATORY CLAIM SUBMISSION :</b><br/>'
|
||||
'In case of non-conformity goods, claims must be submitted within 15 days from the goods pick-up date. Claims received after this time frame will not be processed.'
|
||||
)
|
||||
footer_table_data = [
|
||||
['', ''],
|
||||
[
|
||||
Paragraph('COURIER / FORWARDER NAME<br/><br/><br/><b>WAREHOUSE PICKUP ADDRESS</b>', styles['Normal']),
|
||||
Paragraph(remarks_text, small_footer_style)
|
||||
],
|
||||
]
|
||||
story.append(bottom_table)
|
||||
story.append(Spacer(1, 10)) # Interlinea tra la tabella sopra e la tabella footer
|
||||
# Inserisco la barra gradiente sopra la tabella footer
|
||||
story.append(DetailHeaderBar(doc.width, 24))
|
||||
footer_table = Table(footer_table_data, colWidths=[bottom_table_total_width/2]*2)
|
||||
footer_table.setStyle(TableStyle([
|
||||
# RIMUOVO il background fucsia, la barra gradiente è sopra
|
||||
#('BACKGROUND', (0, 0), (1, 0), colors.HexColor('#f30ad0')),
|
||||
('TEXTCOLOR', (0, 0), (1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (1, 0), 'CENTER'),
|
||||
('VALIGN', (0, 1), (0, 1), 'TOP'), # COURIER / FORWARDER NAME in alto
|
||||
('FONTNAME', (0, 0), (1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (1, 0), 12),
|
||||
]))
|
||||
story.append(footer_table)
|
||||
# Dopo la tabella footer, aggiungo il blocco privacy notes
|
||||
privacy_notes_text = (
|
||||
'<b>PRIVACY NOTES:</b><br/>'
|
||||
'Pursuant to art. 13 of the EU Regulation n. 2016/679, as the Data Controller of your personal data, MORPHEUS ADVISOR SRL informs you that the acquired data will be processed in computerized and non-computerized mode, for the execution of legal or contractual obligations, to guarantee the correct administrative and accounting management and that your data will not be disseminated. For the exercise of your rights, as provided for by the the Regulations, you can contact us:<br/>'
|
||||
'Tel. +39 0577 686678 - info@apitalianluxury.com. The complete information on the processing of personal data is available at the headquarters of the Data Controller.'
|
||||
)
|
||||
privacy_notes_para = Paragraph(privacy_notes_text, ParagraphStyle('privacy_notes', parent=styles['Normal'], fontSize=7, spaceBefore=8, spaceAfter=0))
|
||||
privacy_notes_table = Table(
|
||||
[[privacy_notes_para]],
|
||||
colWidths=[bottom_table_total_width]
|
||||
)
|
||||
privacy_notes_table.setStyle(TableStyle([
|
||||
#('BOX', (0, 0), (-1, -1), 1, colors.black),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 4),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
||||
]))
|
||||
story.append(privacy_notes_table)
|
||||
|
||||
# Barra fuchsia con messaggio "Thank You for Your Business"
|
||||
class ThankYouBar(Flowable):
|
||||
def __init__(self, width):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.height = 30
|
||||
|
||||
def draw(self):
|
||||
self.canv.saveState()
|
||||
# Gradiente da nero (#000000) a fucsia (#f30ad0) molto uniforme
|
||||
steps = int(self.width * 2) # Rettangoli larghi 0.5 punti
|
||||
rect_width = self.width / steps
|
||||
for i in range(steps):
|
||||
ratio = i / (steps - 1)
|
||||
r = int(0 + (243 - 0) * ratio)
|
||||
g = int(0 + (10 - 0) * ratio)
|
||||
b = int(0 + (208 - 0) * ratio)
|
||||
color = colors.Color(r/255, g/255, b/255)
|
||||
self.canv.setFillColor(color)
|
||||
self.canv.rect(i * rect_width, 0, rect_width + 0.5, self.height, stroke=0, fill=1)
|
||||
# Testo centrato
|
||||
self.canv.setFont("Helvetica-Bold", 14)
|
||||
self.canv.setFillColor(colors.white)
|
||||
self.canv.drawCentredString(self.width/2, 8, "Thank You for Your Business")
|
||||
self.canv.restoreState()
|
||||
|
||||
def wrap(self, availWidth, availHeight):
|
||||
return self.width, self.height
|
||||
|
||||
story.append(Spacer(1, 10)) # Spazio prima della barra
|
||||
thank_you_bar = ThankYouBar(doc.width)
|
||||
story.append(thank_you_bar)
|
||||
|
||||
# Alla fine:
|
||||
doc.build(story)
|
||||
pdf_data = buffer.getvalue()
|
||||
buffer.close()
|
||||
|
||||
filename = f'Preventivo_{self.name.replace("/", "_")}_{datetime.now().strftime("%Y%m%d_%H%M")}.pdf'
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(pdf_data),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
'mimetype': 'application/pdf',
|
||||
'description': f'PDF con logo ap_logo_info.png e dati cliente'
|
||||
})
|
||||
|
||||
return {'type': 'ir.actions.act_url', 'url': f'/web/content/{attachment.id}?download=true', 'target': 'new'}
|
||||
|
||||
class SaleOrderLetterheadAdminWizard(models.TransientModel):
|
||||
_name = 'sale.order.letterhead.admin.wizard'
|
||||
_description = 'Wizard Amministrazione: sceglie carta intestata e recupera dati'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', string='Ordine di Vendita', required=True, ondelete='cascade')
|
||||
letterhead_type = fields.Selection([
|
||||
('polo_informatico', 'Polo Informatico'),
|
||||
('metriks', 'Metriks'),
|
||||
('fconn', 'Fconn')
|
||||
], string='Carta Intestata', required=True, default='metriks')
|
||||
|
||||
responsabile_bu = fields.Many2many(
|
||||
'res.users',
|
||||
'sale_order_letterhead_admin_wizard_responsabile_bu_rel',
|
||||
'wizard_id', 'user_id',
|
||||
string='Responsabile BU',
|
||||
required=True
|
||||
)
|
||||
segreteria_commerciale = fields.Many2many(
|
||||
'res.users',
|
||||
'sale_order_letterhead_admin_wizard_segreteria_commerciale_rel',
|
||||
'wizard_id', 'user_id',
|
||||
string='Segreteria Commerciale',
|
||||
required=True
|
||||
)
|
||||
amministrazione = fields.Many2many(
|
||||
'res.users',
|
||||
'sale_order_letterhead_admin_wizard_amministrazione_rel',
|
||||
'wizard_id', 'user_id',
|
||||
string='Amministrazione',
|
||||
required=True
|
||||
)
|
||||
|
||||
responsabile_bu_email = fields.Char(string='Email Responsabile BU', compute='_compute_emails', store=False)
|
||||
segreteria_commerciale_email = fields.Char(string='Email Segreteria Commerciale', compute='_compute_emails', store=False)
|
||||
amministrazione_email = fields.Char(string='Email Amministrazione', compute='_compute_emails', store=False)
|
||||
|
||||
@api.depends('responsabile_bu', 'segreteria_commerciale', 'amministrazione')
|
||||
def _compute_emails(self):
|
||||
for record in self:
|
||||
record.responsabile_bu_email = ', '.join([u.partner_id.email for u in record.responsabile_bu if u.partner_id.email])
|
||||
record.segreteria_commerciale_email = ', '.join([u.partner_id.email for u in record.segreteria_commerciale if u.partner_id.email])
|
||||
record.amministrazione_email = ', '.join([u.partner_id.email for u in record.amministrazione if u.partner_id.email])
|
||||
|
||||
def _validate_user_emails(self):
|
||||
"""Verifica che tutti gli utenti selezionati abbiano un'email valida"""
|
||||
for field, users in [
|
||||
('Responsabile BU', self.responsabile_bu),
|
||||
('Segreteria Commerciale', self.segreteria_commerciale),
|
||||
('Amministrazione', self.amministrazione)
|
||||
]:
|
||||
for user in users:
|
||||
if not user.partner_id.email:
|
||||
raise UserError(f"L'utente selezionato come {field} ({user.name}) non ha un indirizzo email configurato.")
|
||||
|
||||
def action_generate_and_send_pdf(self):
|
||||
self.ensure_one()
|
||||
self._validate_user_emails()
|
||||
pdf_action = self.sale_order_id.action_generate_custom_pdf(
|
||||
letterhead_type=self.letterhead_type,
|
||||
admin_fields=self._prepare_admin_fields()
|
||||
)
|
||||
attachment_id = self.env['ir.attachment'].search([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', self.sale_order_id.id)
|
||||
], order='create_date desc', limit=1)
|
||||
if not attachment_id:
|
||||
raise UserError("Errore nella generazione del PDF")
|
||||
|
||||
recipients = []
|
||||
for user in self.responsabile_bu:
|
||||
if user.partner_id.email:
|
||||
recipients.append(user.partner_id.email)
|
||||
for user in self.segreteria_commerciale:
|
||||
if user.partner_id.email:
|
||||
recipients.append(user.partner_id.email)
|
||||
for user in self.amministrazione:
|
||||
if user.partner_id.email:
|
||||
recipients.append(user.partner_id.email)
|
||||
recipients = list(set(recipients))
|
||||
if not recipients:
|
||||
raise UserError("Nessun destinatario valido trovato. Compilare i campi Responsabile BU, Segreteria Commerciale e Amministrazione.")
|
||||
|
||||
current_user = self.env.user
|
||||
current_user_name = current_user.name or 'Utente Odoo'
|
||||
current_user_email = current_user.partner_id.email or 'internal@metriks.ai'
|
||||
date_str = self.sale_order_id.date_order.strftime('%d/%m/%Y') if self.sale_order_id.date_order else 'N/A'
|
||||
total_formatted = f"€ {self.sale_order_id.amount_total:,.2f}".replace(',', 'TEMP').replace('.', ',').replace('TEMP', '.')
|
||||
body_html = f"""
|
||||
<div style=\"margin: 0px; padding: 0px; font-family: Arial, sans-serif;\">
|
||||
<p style=\"margin: 0px; padding: 0px; font-size: 13px;\">
|
||||
Buongiorno,<br/><br/>
|
||||
In allegato l'ordine di vendita:<br/><br/>
|
||||
<strong>Riferimento ordine:</strong> {self.sale_order_id.name}<br/>
|
||||
<strong>Cliente:</strong> {self.sale_order_id.partner_id.name}<br/>
|
||||
<strong>Data ordine:</strong> {date_str}<br/>
|
||||
<strong>Totale:</strong> {total_formatted}<br/><br/>
|
||||
<div style=\"background-color: #f8f9fa; padding: 15px; border-left: 4px solid #1f4e79; margin: 20px 0;\">
|
||||
<p style=\"margin: 0; font-weight: bold; color: #1f4e79;\">📋 Accesso rapido all'ordine:</p>
|
||||
<p style=\"margin: 5px 0 0 0;\">
|
||||
<a href=\"{self.sale_order_id.get_base_url()}/web#id={self.sale_order_id.id}&model=sale.order&view_type=form\"
|
||||
style=\"background-color: #1f4e79; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px; display: inline-block;\">
|
||||
🔗 Visualizza Ordine in Odoo
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<hr style=\"border: none; border-top: 1px solid #e9ecef; margin: 20px 0;\"/>
|
||||
<p style=\"font-size: 12px; color: #6c757d;\">
|
||||
<em>Questo messaggio è stato generato automaticamente dal sistema Odoo.</em>
|
||||
</p>
|
||||
Cordiali saluti,<br/>
|
||||
<strong>Metriks AI S.r.l. Società Benefit</strong>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
mail_values = {
|
||||
'subject': f'Riferimento ordine - {self.sale_order_id.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': ','.join(recipients),
|
||||
'email_from': f"{current_user_name} <internal@metriks.ai>",
|
||||
'reply_to': current_user_email,
|
||||
'attachment_ids': [(4, attachment_id.id)],
|
||||
'auto_delete': True,
|
||||
}
|
||||
mail = self.env['mail.mail'].create(mail_values)
|
||||
mail.send()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Successo',
|
||||
'message': f'Il PDF è stato inviato con successo a {len(recipients)} destinatari',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def _prepare_admin_fields(self):
|
||||
self.ensure_one()
|
||||
|
||||
if not self.sale_order_id:
|
||||
raise UserError("Ordine di vendita non trovato.")
|
||||
|
||||
so = self.sale_order_id
|
||||
|
||||
tipo_opp_label = so.tipo_opportunita
|
||||
if 'tipo_opportunita' in so._fields and so._fields['tipo_opportunita'].type == 'selection':
|
||||
tipo_opp_label = dict(so._fields['tipo_opportunita'].selection).get(so.tipo_opportunita, so.tipo_opportunita)
|
||||
|
||||
# 2. Gestione per 'prodotto' (stessa identica logica)
|
||||
prodotto_label = so.prodotto
|
||||
if 'prodotto' in so._fields and so._fields['prodotto'].type == 'selection':
|
||||
prodotto_label = dict(so._fields['prodotto'].selection).get(so.prodotto, so.prodotto)
|
||||
|
||||
|
||||
admin_fields = {
|
||||
|
||||
'opportunity_id': so.opportunity_id.name or '',
|
||||
'prodotto': prodotto_label,
|
||||
'tipo_opportunita': tipo_opp_label,
|
||||
|
||||
# --- CORREZIONE: Aggiunto .name per ottenere la label ---
|
||||
'azienda_segnalata': so.azienda_segnalata.name or '',
|
||||
'azienda_segnalata_1': so.azienda_segnalata_1.name or '',
|
||||
'user_id': so.user_id.name or '',
|
||||
'advisor_1': so.advisor_1.name or '',
|
||||
'advisor_2': so.advisor_2.name or '',
|
||||
'specialist_commerciale': so.specialist_commerciale.name or '',
|
||||
'azienda_fatturatrice': so.azienda_fatturatrice.name or '',
|
||||
|
||||
'iban': so.iban,
|
||||
'sdi': so.sdi,
|
||||
'email': getattr(so, 'email', ''),
|
||||
'email_pec': getattr(so, 'email_pec', ''),
|
||||
'signed_on': so.signed_on or '',
|
||||
'canone_tre_anni': so.canone_tre_anni or 0.0,
|
||||
'canone_anno_1': so.canone_anno_1 or 0.0,
|
||||
'canone_anno_2': so.canone_anno_2 or 0.0,
|
||||
'canone_anno_3': so.canone_anno_3 or 0.0,
|
||||
'payment_term_id': so.payment_term_id.name or '',
|
||||
'termini_pagamento_canone': so.termini_pagamento_canone.name or '',
|
||||
'decorrenza_canone': so.decorrenza_canone or '',
|
||||
}
|
||||
|
||||
return admin_fields
|
||||
3
morpheus_custom_pdf_report/security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
||||
# ... existing code ...
|
||||
# (Rimuovere tutte le righe che fanno riferimento a model_sale_order_letterhead_wizard)
|
||||
# ... existing code ...
|
||||
|
BIN
morpheus_custom_pdf_report/static/src/img/ap_logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/ap_logo_info.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/fconn_logo.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/metriks_logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/polo_1.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/polo_2.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
morpheus_custom_pdf_report/static/src/img/polo_3.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
20
morpheus_custom_pdf_report/views/letterhead_wizard_views.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_order_form_letterhead" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.letterhead</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_generate_custom_pdf"
|
||||
string="Genera PDF"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-file-pdf-o"
|
||||
help="Genera un PDF personalizzato"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
morpheus_sale_order_import_lines_v2/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
16
morpheus_sale_order_import_lines_v2/__manifest__.py
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Importa righe e prodotti da file in preventivo",
|
||||
"version": "1.0",
|
||||
"author": "ChatGPT",
|
||||
"category": "Sales",
|
||||
"depends": ["sale", "product"],
|
||||
"data": [
|
||||
# "security/import_sale_order_wizard_model.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/sale_order_view.xml",
|
||||
"views/import_wizard_view.xml",
|
||||
|
||||
],
|
||||
"installable": True,
|
||||
"application": False
|
||||
}
|
||||
1
morpheus_sale_order_import_lines_v2/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import sale_order
|
||||
24
morpheus_sale_order_import_lines_v2/models/sale_order.py
Normal file
@ -0,0 +1,24 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
def action_open_import_wizard(self):
|
||||
return {
|
||||
"name": "Importa Articoli",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "import.sale.order.wizard",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {"default_order_id": self.id},
|
||||
}
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
product_barcode = fields.Char(
|
||||
string='Barcode',
|
||||
related='product_id.barcode',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_import_sale_order_wizard,access.import.sale.order.wizard,model_import_sale_order_wizard,,1,1,1,1
|
||||
|
@ -0,0 +1,12 @@
|
||||
<odoo>
|
||||
<record id="view_order_form_import" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.import</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<header position="inside">
|
||||
<button name="action_open_import_wizard" type="object" string="Importa Articoli" class="btn-primary"/>
|
||||
</header>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@ -0,0 +1,21 @@
|
||||
<odoo>
|
||||
<record id="view_import_sale_order_wizard_form" model="ir.ui.view">
|
||||
<field name="name">import.sale.order.wizard.form</field>
|
||||
<field name="model">import.sale.order.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Importa Articoli nel Preventivo">
|
||||
<group>
|
||||
<field name="order_id" readonly="1"/>
|
||||
<field name="import_file"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="log_message" modifiers="{'invisible': true}" />
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Importa" type="object" name="action_import_lines" class="btn-primary"/>
|
||||
<button string="Annulla" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@ -0,0 +1,35 @@
|
||||
<odoo>
|
||||
<record id="view_order_form_import" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.import</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<header position="inside">
|
||||
<button name="action_open_import_wizard" type="object"
|
||||
string="Importa Articoli" class="btn-primary"/>
|
||||
</header>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Estensione della sottovista delle righe ordine per mostrare il barcode -->
|
||||
<record id="view_order_form_inherit_barcode" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.inherit.barcode</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Inserisce il campo barcode dopo il campo product_id nella sottovista delle righe -->
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="after">
|
||||
<field name="product_barcode" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Azione per aprire il wizard -->
|
||||
<record id="action_open_import_sale_order_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Importa da Excel</field>
|
||||
<field name="res_model">import.sale.order.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_order_id': active_id}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
morpheus_sale_order_import_lines_v2/wizards/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import import_sale_order_wizard
|
||||
@ -0,0 +1,465 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
import base64
|
||||
import io
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
class ImportSaleOrderWizard(models.TransientModel):
|
||||
_name = "import.sale.order.wizard"
|
||||
_description = "Importa Articoli nel Preventivo"
|
||||
|
||||
import_file = fields.Binary("File Excel (.xlsx)", required=True)
|
||||
order_id = fields.Many2one("sale.order", string="Preventivo", required=True)
|
||||
log_message = fields.Text("Log Importazione", readonly=True)
|
||||
|
||||
def action_import_lines(self):
|
||||
if not self.import_file:
|
||||
raise UserError("Nessun file caricato.")
|
||||
|
||||
file_data = base64.b64decode(self.import_file)
|
||||
try:
|
||||
wb = load_workbook(filename=io.BytesIO(file_data), data_only=True)
|
||||
except Exception as e:
|
||||
raise UserError(f"Errore nell'aprire il file Excel: {str(e)}")
|
||||
|
||||
sheet = wb.active
|
||||
|
||||
headers = [str(cell.value).strip() if cell.value else "" for cell in next(sheet.iter_rows(max_row=1))]
|
||||
header_map = {
|
||||
"Codice EAN": "ean",
|
||||
"Nome Prodotto": "name",
|
||||
"Quantità": "qty",
|
||||
"Prezzo Unitario": "price",
|
||||
"Taglia": "size",
|
||||
"Colore": "color"
|
||||
}
|
||||
|
||||
for required_col in header_map:
|
||||
if required_col not in headers:
|
||||
raise UserError(f"Colonna obbligatoria mancante: '{required_col}'")
|
||||
|
||||
col_index = {v: headers.index(k) for k, v in header_map.items()}
|
||||
|
||||
Attribute = self.env["product.attribute"]
|
||||
AttributeValue = self.env["product.attribute.value"]
|
||||
ProductTemplate = self.env["product.template"]
|
||||
ProductProduct = self.env["product.product"]
|
||||
SaleOrderLine = self.env["sale.order.line"]
|
||||
|
||||
log_lines = []
|
||||
|
||||
attr_size = Attribute.search([("name", "=", "Taglia")], limit=1)
|
||||
if not attr_size:
|
||||
attr_size = Attribute.create({"name": "Taglia"})
|
||||
|
||||
attr_color = Attribute.search([("name", "=", "Colore")], limit=1)
|
||||
if not attr_color:
|
||||
attr_color = Attribute.create({"name": "Colore"})
|
||||
|
||||
for row_idx, row in enumerate(sheet.iter_rows(min_row=2), start=2):
|
||||
try:
|
||||
name = str(row[col_index["name"]].value or "").strip()
|
||||
qty = float(row[col_index["qty"]].value or 0)
|
||||
price = float(row[col_index["price"]].value or 0)
|
||||
size = str(row[col_index["size"]].value or "").strip()
|
||||
color = str(row[col_index["color"]].value or "").strip()
|
||||
ean = str(row[col_index["ean"]].value or "").strip()
|
||||
|
||||
if not name or not size or not color:
|
||||
log_lines.append(f"Riga {row_idx}: Dati mancanti (Codice Prodotto, Taglia o Colore)")
|
||||
continue
|
||||
|
||||
val_size = AttributeValue.search([("name", "=", size), ("attribute_id", "=", attr_size.id)], limit=1)
|
||||
if not val_size:
|
||||
val_size = AttributeValue.create({"name": size, "attribute_id": attr_size.id})
|
||||
|
||||
val_color = AttributeValue.search([("name", "=", color), ("attribute_id", "=", attr_color.id)], limit=1)
|
||||
if not val_color:
|
||||
val_color = AttributeValue.create({"name": color, "attribute_id": attr_color.id})
|
||||
|
||||
tmpl = ProductTemplate.search([("name", "=", name)], limit=1)
|
||||
if not tmpl:
|
||||
tmpl = ProductTemplate.create({
|
||||
"name": name,
|
||||
"type": "consu",
|
||||
})
|
||||
tmpl.write({
|
||||
"attribute_line_ids": [(0, 0, {
|
||||
"attribute_id": attr_size.id,
|
||||
"value_ids": [(6, 0, [val_size.id])],
|
||||
}), (0, 0, {
|
||||
"attribute_id": attr_color.id,
|
||||
"value_ids": [(6, 0, [val_color.id])],
|
||||
})]
|
||||
})
|
||||
else:
|
||||
size_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id.id == attr_size.id)
|
||||
if size_line and val_size.id not in size_line.value_ids.ids:
|
||||
size_line.write({"value_ids": [(4, val_size.id)]})
|
||||
elif not size_line:
|
||||
tmpl.write({"attribute_line_ids": [(0, 0, {
|
||||
"attribute_id": attr_size.id,
|
||||
"value_ids": [(6, 0, [val_size.id])]
|
||||
})]})
|
||||
|
||||
color_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id.id == attr_color.id)
|
||||
if color_line and val_color.id not in color_line.value_ids.ids:
|
||||
color_line.write({"value_ids": [(4, val_color.id)]})
|
||||
elif not color_line:
|
||||
tmpl.write({"attribute_line_ids": [(0, 0, {
|
||||
"attribute_id": attr_color.id,
|
||||
"value_ids": [(6, 0, [val_color.id])]
|
||||
})]})
|
||||
|
||||
tmpl._create_variant_ids()
|
||||
|
||||
variants = ProductProduct.search([("product_tmpl_id", "=", tmpl.id)])
|
||||
variant = None
|
||||
for v in variants:
|
||||
values = v.product_template_attribute_value_ids
|
||||
if all([
|
||||
any(val.name == size and val.attribute_id.id == attr_size.id for val in values),
|
||||
any(val.name == color and val.attribute_id.id == attr_color.id for val in values),
|
||||
]):
|
||||
variant = v
|
||||
break
|
||||
|
||||
if not variant:
|
||||
log_lines.append(f"Riga {row_idx}: Variante non trovata per {name} - {size}/{color}")
|
||||
continue
|
||||
|
||||
if ean and ProductProduct.search([("barcode", "=", ean), ("id", "!=", variant.id)], limit=1):
|
||||
log_lines.append(f"Riga {row_idx}: Barcode {ean} già usato da altro prodotto.")
|
||||
continue
|
||||
|
||||
if ean:
|
||||
variant.barcode = ean
|
||||
|
||||
SaleOrderLine.create({
|
||||
"order_id": self.order_id.id,
|
||||
"product_id": variant.id,
|
||||
"product_uom_qty": qty,
|
||||
"price_unit": price,
|
||||
"name": name,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log_lines.append(f"Riga {row_idx}: Errore durante importazione - {str(e)}")
|
||||
|
||||
self.log_message = "\n".join(log_lines) if log_lines else "Importazione completata senza errori."
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "import.sale.order.wizard",
|
||||
"view_mode": "form",
|
||||
"res_id": self.id,
|
||||
"target": "new",
|
||||
"context": self.env.context,
|
||||
}
|
||||
|
||||
|
||||
# from odoo import models, fields, api
|
||||
# from odoo.exceptions import UserError
|
||||
# import base64
|
||||
# import io
|
||||
# import logging
|
||||
# from openpyxl import load_workbook
|
||||
|
||||
# # _logger = logging.getLogger(__name__)
|
||||
|
||||
# class ImportSaleOrderWizard(models.TransientModel):
|
||||
# _name = "import.sale.order.wizard"
|
||||
# _description = "Importa Articoli nel Preventivo"
|
||||
|
||||
# import_file = fields.Binary("File Excel (.xlsx)", required=True)
|
||||
# order_id = fields.Many2one("sale.order", string="Preventivo", required=True)
|
||||
# # log_message = fields.Text("Log Importazione", readonly=True)
|
||||
|
||||
# # def action_import_lines(self):
|
||||
# # _logger.info("⚙️ Metodo action_import_lines chiamato correttamente")
|
||||
# # raise UserError("✅ Il metodo è stato eseguito correttamente.")
|
||||
|
||||
# def action_import_lines(self):
|
||||
# if not self.import_file:
|
||||
# raise UserError("Nessun file caricato.")
|
||||
|
||||
# file_data = base64.b64decode(self.import_file)
|
||||
# try:
|
||||
# wb = load_workbook(filename=io.BytesIO(file_data), data_only=True)
|
||||
# except Exception as e:
|
||||
# raise UserError(f"Errore nell'aprire il file Excel: {str(e)}")
|
||||
|
||||
# sheet = wb.active
|
||||
|
||||
# # Leggi intestazioni reali
|
||||
# headers = [str(cell.value).strip() if cell.value else "" for cell in next(sheet.iter_rows(max_row=1))]
|
||||
# header_map = {
|
||||
# "Codice EAN": "ean",
|
||||
# "Codice Prodotto": "name",
|
||||
# "Quantità": "qty",
|
||||
# "Prezzo Unitario": "price",
|
||||
# "Taglia": "size",
|
||||
# "Colore": "color"
|
||||
# }
|
||||
|
||||
# for required_col in header_map:
|
||||
# if required_col not in headers:
|
||||
# raise UserError(f"Colonna obbligatoria mancante: '{required_col}'")
|
||||
|
||||
# col_index = {v: headers.index(k) for k, v in header_map.items()}
|
||||
|
||||
# Attribute = self.env["product.attribute"]
|
||||
# AttributeValue = self.env["product.attribute.value"]
|
||||
# ProductTemplate = self.env["product.template"]
|
||||
# ProductProduct = self.env["product.product"]
|
||||
# SaleOrderLine = self.env["sale.order.line"]
|
||||
|
||||
# log_lines = []
|
||||
|
||||
# # Recupera o crea attributi
|
||||
# attr_size = Attribute.search([("name", "=", "Taglia")], limit=1)
|
||||
# if not attr_size:
|
||||
# attr_size = Attribute.create({"name": "Taglia"})
|
||||
# attr_color = Attribute.search([("name", "=", "Colore")], limit=1)
|
||||
# if not attr_color:
|
||||
# attr_color = Attribute.create({"name": "Colore"})
|
||||
|
||||
# for row_idx, row in enumerate(sheet.iter_rows(min_row=2), start=2):
|
||||
# try:
|
||||
# name = str(row[col_index["name"]].value or "").strip()
|
||||
# qty = float(row[col_index["qty"]].value or 0)
|
||||
# price = float(row[col_index["price"]].value or 0)
|
||||
# size = str(row[col_index["size"]].value or "").strip()
|
||||
# color = str(row[col_index["color"]].value or "").strip()
|
||||
# ean = str(row[col_index["ean"]].value or "").strip()
|
||||
|
||||
# if not name or not size or not color:
|
||||
# log_lines.append(f"Riga {row_idx}: Dati mancanti (Codice Prodotto, Taglia o Colore)")
|
||||
# continue
|
||||
|
||||
# # Crea valori attributo se non esistono
|
||||
# val_size = AttributeValue.search([("name", "=", size), ("attribute_id", "=", attr_size.id)], limit=1)
|
||||
# if not val_size:
|
||||
# val_size = AttributeValue.create({"name": size, "attribute_id": attr_size.id})
|
||||
|
||||
# val_color = AttributeValue.search([("name", "=", color), ("attribute_id", "=", attr_color.id)], limit=1)
|
||||
# if not val_color:
|
||||
# val_color = AttributeValue.create({"name": color, "attribute_id": attr_color.id})
|
||||
|
||||
# # Cerca o crea template
|
||||
# tmpl = ProductTemplate.search([("name", "=", name)], limit=1)
|
||||
# if not tmpl:
|
||||
# tmpl = ProductTemplate.create({
|
||||
# "name": name,
|
||||
# "type": "product",
|
||||
# "attribute_line_ids": [(0, 0, {
|
||||
# "attribute_id": attr_size.id,
|
||||
# "value_ids": [(6, 0, [val_size.id])],
|
||||
# }), (0, 0, {
|
||||
# "attribute_id": attr_color.id,
|
||||
# "value_ids": [(6, 0, [val_color.id])],
|
||||
# })]
|
||||
# })
|
||||
# else:
|
||||
# # Aggiungi attributi mancanti
|
||||
# size_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id.id == attr_size.id)
|
||||
# if size_line and val_size.id not in size_line.value_ids.ids:
|
||||
# size_line.write({"value_ids": [(4, val_size.id)]})
|
||||
# color_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id.id == attr_color.id)
|
||||
# if color_line and val_color.id not in color_line.value_ids.ids:
|
||||
# color_line.write({"value_ids": [(4, val_color.id)]})
|
||||
|
||||
# tmpl._create_variant_ids()
|
||||
|
||||
# # Cerca variante
|
||||
# variant = ProductProduct.search([
|
||||
# ("product_tmpl_id", "=", tmpl.id),
|
||||
# ("product_template_attribute_value_ids.attribute_id", "in", [attr_size.id, attr_color.id]),
|
||||
# ("product_template_attribute_value_ids.name", "in", [size, color])
|
||||
# ], limit=1)
|
||||
|
||||
# if not variant:
|
||||
# log_lines.append(f"Riga {row_idx}: Variante non trovata per {name} - {size}/{color}")
|
||||
# continue
|
||||
|
||||
# # Verifica se EAN è già usato da altri
|
||||
# if ean and ProductProduct.search([("barcode", "=", ean), ("id", "!=", variant.id)], limit=1):
|
||||
# log_lines.append(f"Riga {row_idx}: Barcode {ean} già usato da altro prodotto.")
|
||||
# continue
|
||||
|
||||
# # Imposta barcode
|
||||
# variant.barcode = ean
|
||||
|
||||
# # Crea riga ordine
|
||||
# SaleOrderLine.create({
|
||||
# "order_id": self.order_id.id,
|
||||
# "product_id": variant.id,
|
||||
# "product_uom_qty": qty,
|
||||
# "price_unit": price,
|
||||
# "name": name,
|
||||
# })
|
||||
|
||||
# except Exception as e:
|
||||
# log_lines.append(f"Riga {row_idx}: Errore durante importazione - {str(e)}")
|
||||
|
||||
# self.log_message = "\n".join(log_lines) if log_lines else "Importazione completata senza errori."
|
||||
|
||||
# return {
|
||||
# "type": "ir.actions.act_window",
|
||||
# "res_model": "import.sale.order.wizard",
|
||||
# "view_mode": "form",
|
||||
# "res_id": self.id,
|
||||
# "target": "new",
|
||||
# "context": self.env.context,
|
||||
# }
|
||||
|
||||
|
||||
##########################################################################################
|
||||
|
||||
# from odoo import models, fields, api
|
||||
# from odoo.exceptions import UserError
|
||||
# import base64
|
||||
# import io
|
||||
# from openpyxl import load_workbook
|
||||
|
||||
# class ImportSaleOrderWizard(models.TransientModel):
|
||||
# _name = "import.sale.order.wizard"
|
||||
# _description = "Importa Articoli nel Preventivo"
|
||||
|
||||
# import_file = fields.Binary("File Excel (.xlsx)", required=True)
|
||||
# order_id = fields.Many2one("sale.order", string="Preventivo", required=True)
|
||||
# log_message = fields.Text("Log Importazione", readonly=True)
|
||||
|
||||
# def action_import_lines(self):
|
||||
# if not self.import_file:
|
||||
# raise UserError("Nessun file caricato.")
|
||||
|
||||
# file_data = base64.b64decode(self.import_file)
|
||||
# try:
|
||||
# wb = load_workbook(filename=io.BytesIO(file_data), data_only=True)
|
||||
# except Exception as e:
|
||||
# raise UserError(f"Errore nell'aprire il file Excel: {str(e)}")
|
||||
|
||||
# sheet = wb.active
|
||||
|
||||
# # Leggi intestazioni in minuscolo e senza spazi iniziali/finali
|
||||
# headers = [str(cell.value).strip().lower() if cell.value else "" for cell in next(sheet.iter_rows(max_row=1))]
|
||||
# # required_headers = ["nome", "descrizione", "prezzo", "quantità", "taglia", "colore", "ean13"]
|
||||
# required_headers = ["codice ean", "codice prodotto", "quantità", "prezzo unitario", "taglia", "colore"]
|
||||
|
||||
# for rh in required_headers:
|
||||
# if rh not in headers:
|
||||
# raise UserError(f"Colonna obbligatoria mancante: '{rh}'")
|
||||
|
||||
# col_index = {h: headers.index(h) for h in required_headers}
|
||||
|
||||
# Attribute = self.env["product.attribute"]
|
||||
# AttributeValue = self.env["product.attribute.value"]
|
||||
# ProductTemplate = self.env["product.template"]
|
||||
# ProductProduct = self.env["product.product"]
|
||||
# SaleOrderLine = self.env["sale.order.line"]
|
||||
|
||||
# log_lines = []
|
||||
|
||||
# attr_size = Attribute.search([("name", "=", "Taglia")], limit=1) or Attribute.create({"name": "Taglia"})
|
||||
# attr_color = Attribute.search([("name", "=", "Colore")], limit=1) or Attribute.create({"name": "Colore"})
|
||||
|
||||
# for row_idx, row in enumerate(sheet.iter_rows(min_row=2), start=2):
|
||||
# try:
|
||||
# values = {}
|
||||
# for h in required_headers:
|
||||
# cell = row[col_index[h]]
|
||||
# values[h.lower()] = str(cell.value).strip() if cell.value is not None else ""
|
||||
|
||||
# name = values["nome"]
|
||||
# descrizione = values["descrizione"]
|
||||
# try:
|
||||
# price = float(values["prezzo"])
|
||||
# qty = float(values["quantità"])
|
||||
# except ValueError:
|
||||
# raise UserError(f"Prezzo o quantità non numerici alla riga {row_idx}")
|
||||
# size = values["taglia"]
|
||||
# color = values["colore"]
|
||||
# ean = values["ean13"]
|
||||
|
||||
# # Cerca o crea valori attributi taglia e colore
|
||||
# val_size = AttributeValue.search([("name", "=", size), ("attribute_id", "=", attr_size.id)], limit=1)
|
||||
# if not val_size:
|
||||
# val_size = AttributeValue.create({"name": size, "attribute_id": attr_size.id})
|
||||
|
||||
# val_color = AttributeValue.search([("name", "=", color), ("attribute_id", "=", attr_color.id)], limit=1)
|
||||
# if not val_color:
|
||||
# val_color = AttributeValue.create({"name": color, "attribute_id": attr_color.id})
|
||||
|
||||
# # Cerca o crea template prodotto
|
||||
# tmpl = ProductTemplate.search([("name", "=", name)], limit=1)
|
||||
# if not tmpl:
|
||||
# tmpl = ProductTemplate.create({
|
||||
# "name": name,
|
||||
# "type": "product",
|
||||
# "description_sale": descrizione,
|
||||
# "attribute_line_ids": [(0, 0, {
|
||||
# "attribute_id": attr_size.id,
|
||||
# "value_ids": [(6, 0, [val_size.id])]
|
||||
# }), (0, 0, {
|
||||
# "attribute_id": attr_color.id,
|
||||
# "value_ids": [(6, 0, [val_color.id])]
|
||||
# })]
|
||||
# })
|
||||
# else:
|
||||
# # Aggiorna attribute_line_ids per includere i valori se non presenti
|
||||
# size_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id == attr_size)
|
||||
# if size_line and val_size.id not in size_line.value_ids.ids:
|
||||
# size_line.write({"value_ids": [(4, val_size.id)]})
|
||||
# color_line = tmpl.attribute_line_ids.filtered(lambda l: l.attribute_id == attr_color)
|
||||
# if color_line and val_color.id not in color_line.value_ids.ids:
|
||||
# color_line.write({"value_ids": [(4, val_color.id)]})
|
||||
|
||||
# # Forza la creazione varianti
|
||||
# tmpl._create_variant_ids()
|
||||
|
||||
# # Cerca variante per taglia e colore
|
||||
# variant = ProductProduct.search([
|
||||
# ("product_tmpl_id", "=", tmpl.id),
|
||||
# ("product_template_attribute_value_ids.attribute_id", "in", [attr_size.id, attr_color.id]),
|
||||
# ("product_template_attribute_value_ids.name", "in", [size, color])
|
||||
# ], limit=1)
|
||||
|
||||
# if not variant:
|
||||
# log_lines.append(f"Riga {row_idx}: Variante non trovata per {name} - {size}/{color}")
|
||||
# continue
|
||||
|
||||
# # Se barcode esiste già per un altro prodotto diverso, skip
|
||||
# if ProductProduct.search([("barcode", "=", ean), ("id", "!=", variant.id)], limit=1):
|
||||
# log_lines.append(f"Riga {row_idx}: Barcode {ean} già associato ad un altro prodotto")
|
||||
# continue
|
||||
|
||||
# # Imposta barcode
|
||||
# variant.barcode = ean
|
||||
|
||||
# # Crea riga ordine vendita
|
||||
# SaleOrderLine.create({
|
||||
# "order_id": self.order_id.id,
|
||||
# "product_id": variant.id,
|
||||
# "product_uom_qty": qty,
|
||||
# "price_unit": price,
|
||||
# "name": descrizione or name,
|
||||
# })
|
||||
|
||||
# except Exception as e:
|
||||
# log_lines.append(f"Riga {row_idx}: Errore durante l'importazione - {str(e)}")
|
||||
|
||||
# if log_lines:
|
||||
# self.log_message = "\n".join(log_lines)
|
||||
# else:
|
||||
# self.log_message = "Importazione completata con successo senza errori."
|
||||
|
||||
# return {
|
||||
# "type": "ir.actions.act_window",
|
||||
# "res_model": "import.sale.order.wizard",
|
||||
# "view_mode": "form",
|
||||
# "res_id": self.id,
|
||||
# "target": "new",
|
||||
# "context": self.env.context,
|
||||
# }
|
||||