diff --git a/commercial_invoice/__init__.py b/commercial_invoice/__init__.py
new file mode 100644
index 0000000..55370b5
--- /dev/null
+++ b/commercial_invoice/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import report
+from . import models
\ No newline at end of file
diff --git a/commercial_invoice/__manifest__.py b/commercial_invoice/__manifest__.py
new file mode 100644
index 0000000..987d97c
--- /dev/null
+++ b/commercial_invoice/__manifest__.py
@@ -0,0 +1,32 @@
+{
+ 'name': 'Commercial Invoice',
+ 'version': '1.0',
+ 'category': 'Accounting',
+ 'summary': 'Generate commercial invoices for cross-border shipments',
+ 'description': """
+ Generate commercial invoices for cross-border shipments between Canada and the USA.
+ Features:
+ - Group multiple invoices into a single commercial invoice
+ - Track additional costs (packaging, freight, insurance)
+ - Print bilingual commercial invoice reports
+ """,
+ 'author': 'marc@bemade.org',
+ 'website': 'https://www.bemade.org',
+ 'license': 'LGPL-3',
+ 'depends': [
+ 'account',
+ 'stock_delivery',
+ 'mail',
+ ],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/commercial_invoice_sequence.xml',
+ 'report/commercial_invoice_report.xml',
+ 'report/report_templates.xml',
+ 'views/account_move_views.xml',
+ 'views/commercial_invoice_views.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+}
diff --git a/commercial_invoice/data/commercial_invoice_sequence.xml b/commercial_invoice/data/commercial_invoice_sequence.xml
new file mode 100644
index 0000000..1d8cf7e
--- /dev/null
+++ b/commercial_invoice/data/commercial_invoice_sequence.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Commercial Invoice
+ commercial.invoice
+ CI/%(year)s/
+ 4
+
+
+
+
diff --git a/commercial_invoice/models/__init__.py b/commercial_invoice/models/__init__.py
new file mode 100644
index 0000000..72ab9cb
--- /dev/null
+++ b/commercial_invoice/models/__init__.py
@@ -0,0 +1,2 @@
+from . import commercial_invoice
+from . import account_move
diff --git a/commercial_invoice/models/account_move.py b/commercial_invoice/models/account_move.py
new file mode 100644
index 0000000..06afe0b
--- /dev/null
+++ b/commercial_invoice/models/account_move.py
@@ -0,0 +1,15 @@
+from odoo import models
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ def action_create_commercial_invoice(self):
+ """Create a commercial invoice from selected invoices."""
+ commercial_invoice = self.env['commercial.invoice'].create_from_invoices(self)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'commercial.invoice',
+ 'res_id': commercial_invoice.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
diff --git a/commercial_invoice/models/commercial_invoice.py b/commercial_invoice/models/commercial_invoice.py
new file mode 100644
index 0000000..dd58ee2
--- /dev/null
+++ b/commercial_invoice/models/commercial_invoice.py
@@ -0,0 +1,114 @@
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+class CommercialInvoice(models.Model):
+ _name = 'commercial.invoice'
+ _description = 'Commercial Invoice for Export'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'date desc, id desc'
+
+ name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default='New')
+ date = fields.Date(string='Export Date', required=True, default=fields.Date.context_today)
+ state = fields.Selection([
+ ('draft', 'Draft'),
+ ('done', 'Done'),
+ ('cancelled', 'Cancelled')
+ ], string='Status', default='draft', tracking=True)
+
+ # Related parties
+ partner_id = fields.Many2one('res.partner', string='Consignee', required=True)
+ importer_id = fields.Many2one('res.partner', string='Importer of Record')
+ customs_broker_id = fields.Many2one('res.partner', string='Customs Broker')
+ company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
+ currency_id = fields.Many2one('res.currency', string='Currency', required=True,
+ default=lambda self: self.env.company.currency_id)
+ related_parties = fields.Boolean(string='Related Parties', default=False)
+
+
+ # Invoice lines and related fields
+ invoice_ids = fields.Many2many('account.move', string='Invoices', domain=[('move_type', '=', 'out_invoice')])
+ payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms')
+ incoterm_id = fields.Many2one('account.incoterms', string='Incoterms')
+
+ # Shipping details
+ number_of_packages = fields.Integer(string='Number of Packages')
+ total_weight = fields.Float(string='Total Weight (kg)')
+ packaging_cost = fields.Monetary(string='Packaging Cost', currency_field='currency_id')
+ freight_cost = fields.Monetary(string='Freight Cost', currency_field='currency_id')
+ insurance_cost = fields.Monetary(string='Insurance Cost', currency_field='currency_id')
+ other_cost = fields.Monetary(string='Other Costs', currency_field='currency_id')
+
+ # Computed fields
+ invoice_amount = fields.Monetary(string='Invoice Amount', currency_field='currency_id',
+ compute='_compute_amounts', store=True)
+ total_amount = fields.Monetary(string='Total Amount', currency_field='currency_id',
+ compute='_compute_amounts', store=True)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code('commercial.invoice') or 'New'
+ return super().create(vals_list)
+
+ @api.depends('invoice_ids', 'packaging_cost', 'freight_cost', 'insurance_cost', 'other_cost')
+ def _compute_amounts(self):
+ for record in self:
+ record.invoice_amount = sum(record.invoice_ids.mapped('amount_total'))
+ record.total_amount = (record.invoice_amount + record.packaging_cost +
+ record.freight_cost + record.insurance_cost + record.other_cost)
+
+ def action_confirm(self):
+ self.write({'state': 'done'})
+
+ def action_draft(self):
+ self.write({'state': 'draft'})
+
+ def action_cancel(self):
+ self.write({'state': 'cancelled'})
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id(self):
+ if self.partner_id:
+ self.payment_term_id = self.partner_id.property_payment_term_id
+
+ @api.model
+ def _prepare_commercial_invoice_from_invoices(self, invoices):
+ """Prepare commercial invoice values from a set of invoices."""
+ if not invoices:
+ raise UserError(_("No invoices selected."))
+
+ # Get unique values for key fields
+ currencies = invoices.mapped('currency_id')
+ payment_terms = invoices.mapped('invoice_payment_term_id')
+ incoterms = invoices.mapped('invoice_incoterm_id')
+ companies = invoices.mapped('company_id')
+
+ # Validate consistency
+ if len(currencies) > 1:
+ raise UserError(_("Selected invoices have different currencies."))
+ if len(companies) > 1:
+ raise UserError(_("Selected invoices are from different companies."))
+
+ # Get shipping and billing partners
+ shipping_partners = invoices.mapped('partner_shipping_id.commercial_partner_id')
+ billing_partners = invoices.mapped('partner_id.commercial_partner_id')
+
+ # Prepare values
+ vals = {
+ 'invoice_ids': [(6, 0, invoices.ids)],
+ 'company_id': companies[0].id,
+ 'currency_id': currencies[0].id,
+ 'partner_id': shipping_partners[0].id if len(shipping_partners) == 1 else False,
+ 'importer_id': billing_partners[0].id if len(billing_partners) == 1 else False,
+ 'payment_term_id': payment_terms[0].id if len(payment_terms) == 1 else False,
+ 'incoterm_id': incoterms[0].id if len(incoterms) == 1 else False,
+ }
+
+ return vals
+
+ @api.model
+ def create_from_invoices(self, invoices):
+ """Create a commercial invoice from a set of invoices."""
+ vals = self._prepare_commercial_invoice_from_invoices(invoices)
+ return self.create(vals)
diff --git a/commercial_invoice/report/__init__.py b/commercial_invoice/report/__init__.py
new file mode 100644
index 0000000..67dee8c
--- /dev/null
+++ b/commercial_invoice/report/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
diff --git a/commercial_invoice/report/commercial_invoice_report.xml b/commercial_invoice/report/commercial_invoice_report.xml
new file mode 100644
index 0000000..617b2be
--- /dev/null
+++ b/commercial_invoice/report/commercial_invoice_report.xml
@@ -0,0 +1,13 @@
+
+
+
+ Commercial Invoice
+ commercial.invoice
+ qweb-pdf
+ commercial_invoice.report_commercial_invoice
+ commercial_invoice.report_commercial_invoice
+ 'Commercial Invoice - %s' % object.name
+
+ report
+
+
diff --git a/commercial_invoice/report/report_templates.xml b/commercial_invoice/report/report_templates.xml
new file mode 100644
index 0000000..cea7892
--- /dev/null
+++ b/commercial_invoice/report/report_templates.xml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Commercial Invoice / Facture Commerciale
+
+
+
+
+
+ Export Date / Date d'exportation:
+
+ |
+
+ Invoice # / No. de facture:
+
+ |
+
+ Related Parties / Parties liées:
+
+ |
+
+
+
+
+
+
+
+ Shipper/Exporter - Expéditeur/Exportateur:
+
+ Tax ID:
+ |
+
+ Consignee - Destinataire:
+
+ Tax ID:
+ |
+
+
+
+
+
+
+
+ Country of Export / Pays d'exportation:
+
+ |
+
+ Terms of Sale / Conditions de vente:
+
+ |
+
+ Currency / Devise:
+
+ |
+
+
+
+
+
+
+
+ Country of Origin / Pays d'origine:
+
+
+
+ |
+
+ Importer of Record / Importateur attitré:
+ (if different than consignee / si différent du destinataire)
+
+
+
+ |
+
+
+
+ Country of Ultimate Destination / Pays de destination finale:
+
+ |
+
+
+
+
+
+
+
+ Item # & Description No. d'article & description |
+ Client Item # No. d'article client |
+ HS # No. SH |
+ Quantity Quantité |
+ Unit Value Valeur unitaire |
+ Total Value Valeur totale |
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+ These commodities, technology or software were exported from Canada in accordance with the Export
+ Administration Regulations. Diversion contrary to Canadian law prohibited.
+ It is hereby certified that this invoice shows the actual price of the goods described, that no other invoice has
+ been issued and that all particulars are true and correct.
+ ----------------------------------------------------------------------------
+ Ces marchandises, technologies ou logiciels ont été exportés du Canada conformément aux règlements
+ administratifs sur l'exportation des Etats-Unis. Tout agissement contraire à la loi Canadienne est strictement
+ interdit.
+ Je certifie par la présente que les prix indiqués sur cette facture sont exacts, qu'aucune autre facture
+ commerciale n'a été produite et que tous les renseignements fournis sont véridiques.
+
+
+
+
Signature: _______________________
+
Title / Titre: _______________________
+
Date: _______________________
+
+
+
+
+
+ | Packaging / Emballage: |
+
+
+ |
+
+
+ | Freight / Fret: |
+
+
+ |
+
+
+ | Insurance / Assurance: |
+
+
+ |
+
+
+ | Other / Autre: |
+
+
+ |
+
+
+ | Total Invoice Value / Valeur totale de la facture: |
+
+
+ |
+
+
+
+
+
+
+
+
+
+ Customs Broker / Courtier en douane:
+
+ |
+
+ Number of Packages / Nombre de colis:
+
+ |
+
+ Total Weight / Poids total:
+ kg
+ |
+
+
+ |
+ Incoterms:
+
+ -
+
+ |
+
+
+
+
+
+
diff --git a/commercial_invoice/security/ir.model.access.csv b/commercial_invoice/security/ir.model.access.csv
new file mode 100644
index 0000000..2eded1a
--- /dev/null
+++ b/commercial_invoice/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_commercial_invoice_user,commercial.invoice.user,model_commercial_invoice,account.group_account_invoice,1,1,1,1
+access_commercial_invoice_manager,commercial.invoice.manager,model_commercial_invoice,account.group_account_manager,1,1,1,1
diff --git a/commercial_invoice/views/account_move_views.xml b/commercial_invoice/views/account_move_views.xml
new file mode 100644
index 0000000..9351648
--- /dev/null
+++ b/commercial_invoice/views/account_move_views.xml
@@ -0,0 +1,17 @@
+
+
+
+ account.move.list.commercial.invoice
+ account.move
+
+
+
+
+
+
+
+
diff --git a/commercial_invoice/views/commercial_invoice_views.xml b/commercial_invoice/views/commercial_invoice_views.xml
new file mode 100644
index 0000000..2d6c28e
--- /dev/null
+++ b/commercial_invoice/views/commercial_invoice_views.xml
@@ -0,0 +1,128 @@
+
+
+
+
+ commercial.invoice.tree
+ commercial.invoice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ commercial.invoice.form
+ commercial.invoice
+
+
+
+
+
+
+
+ commercial.invoice.search
+ commercial.invoice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Commercial Invoices
+ commercial.invoice
+ list,form
+ {'search_default_draft': 1}
+
+
+ Create your first commercial invoice
+
+
+
+
+
+
+
+
+
diff --git a/qc_partner_default_lang/__init__.py b/qc_partner_default_lang/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/qc_partner_default_lang/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/qc_partner_default_lang/__manifest__.py b/qc_partner_default_lang/__manifest__.py
new file mode 100644
index 0000000..fa025b0
--- /dev/null
+++ b/qc_partner_default_lang/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ 'name': 'Partner Default Language by Location',
+ 'version': '18.0.1.0.0',
+ 'category': 'Tools',
+ 'summary': 'Automatically set partner language based on state/country',
+ 'author': 'Bemade',
+ 'website': 'https://www.bemade.org',
+ 'depends': ['base'],
+ 'data': [],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+ 'license': 'LGPL-3',
+}
diff --git a/qc_partner_default_lang/models/__init__.py b/qc_partner_default_lang/models/__init__.py
new file mode 100644
index 0000000..91fed54
--- /dev/null
+++ b/qc_partner_default_lang/models/__init__.py
@@ -0,0 +1 @@
+from . import res_partner
diff --git a/qc_partner_default_lang/models/res_partner.py b/qc_partner_default_lang/models/res_partner.py
new file mode 100644
index 0000000..7dd1b13
--- /dev/null
+++ b/qc_partner_default_lang/models/res_partner.py
@@ -0,0 +1,34 @@
+from odoo import api, models
+
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ # Only set language if it's not already specified
+ if not vals.get('lang'):
+ state = vals.get('state_id')
+ country = vals.get('country_id')
+
+ if state:
+ state_rec = self.env['res.country.state'].browse(state)
+ # Check if the state is Quebec
+ if state_rec.code == 'QC' and state_rec.country_id.code == 'CA':
+ vals['lang'] = 'fr_CA'
+ else:
+ vals['lang'] = 'en_US'
+ else:
+ vals['lang'] = 'en_US'
+
+ return super().create(vals_list)
+
+ @api.onchange('state_id', 'country_id')
+ def _onchange_location_set_lang(self):
+ # Only suggest language change if it's a new record (id not set yet)
+ if not self.id and not self.lang:
+ if self.state_id and self.state_id.code == 'QC' and self.state_id.country_id.code == 'CA':
+ self.lang = 'fr_CA'
+ else:
+ self.lang = 'en_US'
diff --git a/qc_partner_default_lang/static/description/index.html b/qc_partner_default_lang/static/description/index.html
new file mode 100644
index 0000000..f9fc0a1
--- /dev/null
+++ b/qc_partner_default_lang/static/description/index.html
@@ -0,0 +1,27 @@
+
+
+
Partner Default Language by Location
+
Automatic Language Assignment for Partners
+
+
+ This module automatically sets the default language for new partners based on their location.
+
+
+
+
+
+
+
+
+
Features
+
+
+ - Automatically sets French (fr_CA) for partners in Quebec
+ - Defaults to English (en_US) for all other locations
+ - Works on partner creation and when updating location fields
+ - Respects manually set language preferences
+
+
+
+
+]]>