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 @@ + + + + + + + + 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
  • +
+
+
+
+
]]>