diff --git a/l10n_br_account/models/account_move.py b/l10n_br_account/models/account_move.py index b294fd438d6d..290feef3c7e6 100644 --- a/l10n_br_account/models/account_move.py +++ b/l10n_br_account/models/account_move.py @@ -300,6 +300,7 @@ def _compute_amount(self): sign = 1 inv_line_ids = move.line_ids.filtered( lambda line: line.display_type == "product" + and (not line.cfop_id or line.cfop_id.finance_move) ) move.amount_untaxed = sum(inv_line_ids.mapped("amount_untaxed")) move.amount_tax = sum(inv_line_ids.mapped("amount_tax")) @@ -338,31 +339,18 @@ def _compute_needed_terms(self): pass else: untaxed_amount_currency += line.price_subtotal - for tax_result in (line.compute_all_tax or {}).values(): - tax_amount_currency += -sign * tax_result.get( - "amount_currency", 0.0 - ) + for tax_result in (line.compute_all_tax or {}).values(): + tax_amount_currency += -sign * tax_result.get( + "amount_currency", 0.0 + ) untaxed_amount = untaxed_amount_currency tax_amount = tax_amount_currency else: tax_amount_currency = invoice.amount_tax * sign tax_amount = invoice.amount_tax_signed - if invoice.fiscal_operation_id: - if invoice.fiscal_operation_id.deductible_taxes: - amount_currency = ( - invoice.amount_total - + invoice.amount_tax_withholding - ) - else: - amount_currency = ( - invoice.amount_total - invoice.amount_ipi_value - ) * sign - untaxed_amount_currency = amount_currency * sign - untaxed_amount = amount_currency * sign - else: - untaxed_amount_currency = invoice.amount_untaxed * sign - untaxed_amount = invoice.amount_untaxed_signed + untaxed_amount_currency = invoice.amount_untaxed * sign + untaxed_amount = invoice.amount_untaxed_signed invoice_payment_terms = ( invoice.invoice_payment_term_id._compute_terms( date_ref=invoice.invoice_date diff --git a/l10n_br_account/models/account_move_line.py b/l10n_br_account/models/account_move_line.py index 315b2f21dbf6..245a25720091 100644 --- a/l10n_br_account/models/account_move_line.py +++ b/l10n_br_account/models/account_move_line.py @@ -110,6 +110,18 @@ class AccountMoveLine(models.Model): " and '3-3' for the last installment.", ) + # These fields are already inherited by _inherits, but there is some limitation of + # the ORM that the values of these fields are zeroed when called by onchange. This + # limitation directly affects the _get_amount_credit_debit method. + amount_untaxed = fields.Monetary(compute="_compute_amounts") + amount_total = fields.Monetary( + compute="_compute_amounts", + ) + + def _compute_amounts(self): + res = super()._compute_amounts() + return res + @api.depends( "quantity", "price_unit", @@ -301,6 +313,20 @@ def changed(fname): else: # BRAZIL CASE: if line.cfop_id and not line.cfop_id.finance_move: unsigned_amount_currency = 0 + if not line.move_id.fiscal_operation_id.deductible_taxes: + # Quando não há financeiro, mas há imposto, e não há + # dedutíveis, é necessário registrar a contrapartida dos + # impostos para equilibrar o balanço. Na versão 14, essa + # diferença era automaticamente alocada às contas dos + # termos de pagamento. + # TODO: + # o correto mesmo seria não lançar os impostos nesse caso, + # resolver depois. + unsigned_amount_currency = -( + line.amount_tax_included + + line.amount_tax_not_included + - line.amount_tax_withholding + ) else: if line.move_id.fiscal_operation_id.deductible_taxes: unsigned_amount_currency = ( @@ -312,12 +338,10 @@ def changed(fname): ) unsigned_amount_currency = line.currency_id.round( amount_total - - ( - line.amount_tax_included - - line.amount_tax_withholding - ) + - line.amount_tax_included - line.amount_tax_not_included - - line.icms_relief_value + if line.tax_ids + else amount_total ) amount_currency = unsigned_amount_currency * line.move_id.direction_sign if line.amount_currency != amount_currency or line not in before: @@ -325,6 +349,19 @@ def changed(fname): if line.currency_id == line.company_id.currency_id: line.balance = amount_currency + # Os totais nas linhas foram atualizadas, mas o total da fatura + # não foi recalculado automaticamente, já que o método compute_amount + # não foi acionado após as alterações nas linhas. + # Por esse motivo, estou adicionando manualmente os campos no + # add_to_compute do account_move. + # Questão: Por que o compute_amount não foi acionado automaticamente? + # Isso ocorre apenas quando os valores são diretamente informados + # no create? Realizar um teste isolado para confirmar esse + # comportamento. + move_id = line.move_id + self.env.add_to_compute(move_id._fields["amount_total"], move_id) + self.env.add_to_compute(move_id._fields["amount_untaxed"], move_id) + after = existing() for line in after: if ( @@ -394,7 +431,6 @@ def _compute_totals(self): line.price_subtotal = taxes_res["total_excluded"] line.price_total = taxes_res["total_included"] - line._compute_balance() line.price_total += ( line.insurance_value @@ -521,19 +557,33 @@ def _onchange_fiscal_document_line_id(self): # override the default product uom (set by the onchange): self.product_uom_id = self.fiscal_document_line_id.uom_id.id - @api.onchange("fiscal_tax_ids") - def _onchange_fiscal_tax_ids(self): - """Ao alterar o campo fiscal_tax_ids que contém os impostos fiscais, - são atualizados os impostos contábeis relacionados""" - result = super()._onchange_fiscal_tax_ids() + @api.depends("product_id", "product_uom_id", "fiscal_tax_ids") + def _compute_tax_ids(self): + # Adding 'fiscal_tax_ids' as a dependency to ensure that the taxes + # are recalculated when this field changes. + return super()._compute_tax_ids() - # Atualiza os impostos contábeis relacionados aos impostos fiscais - user_type = "sale" - if self.move_id.move_type in ("in_invoice", "in_refund"): + def _get_computed_taxes(self): + """ + Override the native method to load taxes from the fiscal module. + """ + self.ensure_one() + + # If no fiscal operation is defined, fallback to the default implementation. + if not self.fiscal_operation_id: + return super()._get_computed_taxes() + + # Determine the user type based on the document type. + user_type = None + if self.move_id.is_sale_document(include_receipts=True): + user_type = "sale" + elif self.move_id.is_purchase_document(include_receipts=True): user_type = "purchase" - self.tax_ids = self.fiscal_tax_ids.account_taxes( - user_type=user_type, fiscal_operation=self.fiscal_operation_id - ) + # Retrieve taxes based on user type and fiscal operation. + if user_type: + tax_ids = self.fiscal_tax_ids.account_taxes( + user_type=user_type, fiscal_operation=self.fiscal_operation_id + ) - return result + return tax_ids diff --git a/l10n_br_account/tests/test_account_move_lc.py b/l10n_br_account/tests/test_account_move_lc.py index af967744e948..3ce4795d3c92 100644 --- a/l10n_br_account/tests/test_account_move_lc.py +++ b/l10n_br_account/tests/test_account_move_lc.py @@ -666,8 +666,8 @@ def test_simples_remessa(self): "price_total": 1050.0, "tax_line_id": False, "currency_id": self.company_data["currency"].id, - "amount_currency": 0.0, - "debit": 0.0, + "amount_currency": 206.5, + "debit": 206.5, "credit": 0.0, "date_maturity": False, } @@ -775,6 +775,8 @@ def test_simples_remessa(self): "date_maturity": False, } + # Remessa não gera financeiro, as linhas das condições de pagamento + # devem estar zeradas! term_line_vals_1 = { "name": "", "product_id": False, @@ -789,8 +791,8 @@ def test_simples_remessa(self): "tax_ids": [], "tax_line_id": False, "currency_id": self.company_data["currency"].id, - "amount_currency": 206.5, - "debit": 206.5, + "amount_currency": 0, + "debit": 0, "credit": 0.0, "date_maturity": fields.Date.from_string("2019-01-01"), } @@ -803,9 +805,9 @@ def test_simples_remessa(self): "fiscal_position_id": False, "payment_reference": "", "invoice_payment_term_id": self.pay_terms_a.id, - "amount_untaxed": 1000, - "amount_tax": 50, - "amount_total": 206.5, + "amount_untaxed": 0.0, + "amount_tax": 0.0, + "amount_total": 0.0, } self.assertInvoiceValues( @@ -1271,9 +1273,9 @@ def test_simples_remessa_tax_withholding(self): "price_total": 1050.0, "tax_line_id": False, "currency_id": self.company_data["currency"].id, - "amount_currency": 0.0, + "amount_currency": 133.5, "debit": 0.0, - "credit": 0.0, + "credit": 133.5, "date_maturity": False, } @@ -1390,8 +1392,8 @@ def test_simples_remessa_tax_withholding(self): "tax_ids": [], "tax_line_id": False, "currency_id": self.company_data["currency"].id, - "amount_currency": 133.5, - "debit": 133.5, + "amount_currency": 0.0, + "debit": 0.0, "credit": 0.0, "date_maturity": fields.Date.from_string("2019-01-01"), } @@ -1404,9 +1406,9 @@ def test_simples_remessa_tax_withholding(self): "fiscal_position_id": False, "payment_reference": "", "invoice_payment_term_id": self.pay_terms_a.id, - "amount_untaxed": 1000.0, # FIXME is this correct for a simples remessa?? - "amount_tax": 50.0, - "amount_total": 133.5, + "amount_untaxed": 0.0, + "amount_tax": 0.0, + "amount_total": 0.0, } self.assertInvoiceValues( diff --git a/l10n_br_fiscal/models/document_line_mixin_methods.py b/l10n_br_fiscal/models/document_line_mixin_methods.py index 0442dbd36b41..0e4e4c8bbe33 100644 --- a/l10n_br_fiscal/models/document_line_mixin_methods.py +++ b/l10n_br_fiscal/models/document_line_mixin_methods.py @@ -127,6 +127,7 @@ def _get_view(self, view_id=None, view_type="form", **options): "company_id", "price_unit", "quantity", + "icms_relief_id", ) def _compute_amounts(self): for record in self: @@ -134,20 +135,20 @@ def _compute_amounts(self): # Total value of products or services record.price_gross = round_curr.round(record.price_unit * record.quantity) - - record.amount_untaxed = record.price_gross - record.discount_value - record.amount_fiscal = record.price_gross - record.discount_value - record.amount_tax = record.amount_tax_not_included add_to_amount = sum(record[a] for a in record._add_fields_to_amount()) rm_to_amount = sum(record[r] for r in record._rm_fields_to_amount()) + record.amount_untaxed = ( + record.price_gross + - record.discount_value + + add_to_amount + - rm_to_amount + ) # Valor do documento (NF) - record.amount_total = ( - record.amount_untaxed + record.amount_tax + add_to_amount - rm_to_amount - ) + record.amount_total = record.amount_untaxed + record.amount_tax # Valor Liquido (TOTAL + IMPOSTOS - RETENÇÕES) record.amount_taxed = record.amount_total - record.amount_tax_withholding diff --git a/l10n_br_fiscal/models/tax.py b/l10n_br_fiscal/models/tax.py index 1a583402aa80..773d7d67ef06 100644 --- a/l10n_br_fiscal/models/tax.py +++ b/l10n_br_fiscal/models/tax.py @@ -699,19 +699,15 @@ def compute_taxes(self, **kwargs): except AttributeError: taxes[tax.tax_domain].update(tax._compute_tax(tax, taxes, **kwargs)) - if taxes[tax.tax_domain]["tax_include"]: - result_amounts["amount_included"] += taxes[tax.tax_domain].get( - "tax_value", 0.00 - ) + tax_domain = taxes[tax.tax_domain] + tax_value = tax_domain.get("tax_value", 0.00) + if tax_domain["tax_withholding"]: + result_amounts["amount_withholding"] += tax_value else: - result_amounts["amount_not_included"] += taxes[tax.tax_domain].get( - "tax_value", 0.00 - ) - - if taxes[tax.tax_domain]["tax_withholding"]: - result_amounts["amount_withholding"] += taxes[tax.tax_domain].get( - "tax_value", 0.00 - ) + if tax_domain["tax_include"]: + result_amounts["amount_included"] += tax_value + else: + result_amounts["amount_not_included"] += tax_value # Estimate taxes result_amounts["estimate_tax"] = self._compute_estimate_taxes(**kwargs) diff --git a/l10n_br_purchase/models/purchase_order_line.py b/l10n_br_purchase/models/purchase_order_line.py index e63fe87caba5..1f44ade47237 100644 --- a/l10n_br_purchase/models/purchase_order_line.py +++ b/l10n_br_purchase/models/purchase_order_line.py @@ -104,7 +104,6 @@ def _compute_amount(self): { "price_subtotal": line.amount_untaxed, "price_tax": line.amount_tax, - "price_gross": line.amount_untaxed + line.discount_value, "price_total": line.amount_total, } ) diff --git a/l10n_br_sale_stock/README.rst b/l10n_br_sale_stock/README.rst new file mode 100644 index 000000000000..13009c321b66 --- /dev/null +++ b/l10n_br_sale_stock/README.rst @@ -0,0 +1,146 @@ +========================================== +Brazilian Localization Sales and Warehouse +========================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6caf227edb60b685df07f1b80681e366eae20f0674437e09fb2000c812a0ca84 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--brazil-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-brazil/tree/16.0/l10n_br_sale_stock + :alt: OCA/l10n-brazil +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-brazil-16-0/l10n-brazil-16-0-l10n_br_sale_stock + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-brazil&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Este módulo estende o módulo padrão de vendas e estoque do Odoo para +atender às necessidades específicas do Brasil, especialmente no contexto +de vendas de produtos com NF-e (Nota Fiscal Eletrônica). + +Ele automatiza a propagação da operação fiscal, comentários fiscais e +incoterms para as faturas geradas diretamente a partir de ordens de +venda ou movimentações de estoque, garantindo que essas informações +sejam corretamente transferidas e utilizadas no processo de faturamento. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module depends on: + +- sale_stock +- l10n_br_sale +- l10n_br_stock_account + +Configuration +============= + +No configuration required. + +Usage +===== + + + +Known issues / Roadmap +====================== + + + +Changelog +========= + +15.0.1.0.0 (2024-09-17) +----------------------- + +- [MIG] Migration to version 15.0 + +12.0.1.0.0 (2020-05-29) +----------------------- + +- [MIG] Migration to version 12.0 + +10.0.1.0.0 (2019-09-13) +----------------------- + +- [MIG] Migration to version 10.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Renato Lima +- Raphaël Valyi +- Magno Costa +- Gabriel Cardoso de Faria + +Other credits +------------- + +The development of this module has been financially supported by: + +- Aketion LTDA + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-renatonlima| image:: https://github.com/renatonlima.png?size=40px + :target: https://github.com/renatonlima + :alt: renatonlima +.. |maintainer-mbcosta| image:: https://github.com/mbcosta.png?size=40px + :target: https://github.com/mbcosta + :alt: mbcosta + +Current `maintainers `__: + +|maintainer-renatonlima| |maintainer-mbcosta| + +This module is part of the `OCA/l10n-brazil `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_br_sale_stock/__init__.py b/l10n_br_sale_stock/__init__.py new file mode 100644 index 000000000000..371b71bcf51a --- /dev/null +++ b/l10n_br_sale_stock/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2013 Raphaël Valyi - Akretion +# Copyright (C) 2013 Renato Lima - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import models +from . import wizards diff --git a/l10n_br_sale_stock/__manifest__.py b/l10n_br_sale_stock/__manifest__.py new file mode 100644 index 000000000000..36634b4db0f4 --- /dev/null +++ b/l10n_br_sale_stock/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2013 Raphaël Valyi - Akretion +# Copyright (C) 2013 Renato Lima - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +{ + "name": "Brazilian Localization Sales and Warehouse", + "category": "Localization", + "license": "AGPL-3", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-brazil", + "version": "16.0.1.0.0", + "development_status": "Beta", + "maintainers": ["renatonlima", "mbcosta"], + "depends": [ + "sale_stock", + "l10n_br_sale", + "l10n_br_stock_account", + ], + "data": [ + "views/res_company_view.xml", + "views/res_config_settings_view.xml", + "views/sale_order_view.xml", + ], + "demo": [ + "demo/l10n_br_sale_stock_demo.xml", + "demo/sale_order_demo.xml", + ], + "installable": True, + "auto_install": True, +} diff --git a/l10n_br_sale_stock/demo/l10n_br_sale_stock_demo.xml b/l10n_br_sale_stock/demo/l10n_br_sale_stock_demo.xml new file mode 100644 index 000000000000..8e1f6e8042dc --- /dev/null +++ b/l10n_br_sale_stock/demo/l10n_br_sale_stock_demo.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_br_sale_stock/demo/sale_order_demo.xml b/l10n_br_sale_stock/demo/sale_order_demo.xml new file mode 100644 index 000000000000..cce86702439a --- /dev/null +++ b/l10n_br_sale_stock/demo/sale_order_demo.xml @@ -0,0 +1,367 @@ + + + + + + + + True + + + + + + + + + + + + + + l10n_br_sale_stock - Endereço de Entrega e Faturamento diferentes + + + + + + + + draft + + + True + TESTE - TERMOS E CONDIÇÕES + TESTE - CUSTOMER ADDITIONAL DATA + TESTE - FISCAL ADDITIONAL DATA + + + + + Gaveta Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 1 + + 50 + out + + + + + + + + + + + l10n_br_sale_stock - Produto e Serviço + + + + + + + + draft + + + True + TESTE de criação de duas Notas de Serviço e Produto + TESTE - CUSTOMER ADDITIONAL DATA + TESTE - FISCAL ADDITIONAL DATA + + + + + Gaveta Preta + + 2 + + 500 + out + + + + + + + + + + + Customized Odoo Development + + 10 + + 100 + out + + + + + + + + + + + l10n_br_sale_stock - Agrupamento dos Pickings + + + + + + + + draft + + + True + TESTE - TERMOS E CONDIÇÕES + TESTE - CUSTOMER ADDITIONAL DATA + TESTE - FISCAL ADDITIONAL DATA + + + + + [FURN_8900] Gaveta Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 1 + + 50 + out + + + + + + + + + + + l10n_br_sale_stock - Agrupamento dos Pickings + + + + + + + + draft + + + True + TESTE - TERMOS E CONDIÇÕES + TESTE - CUSTOMER ADDITIONAL DATA + TESTE - FISCAL ADDITIONAL DATA + + + + + Gaveta Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 1 + + 50 + out + + + + + + + + + + + + LC l10n_br_sale - Produtos + + + + + + + draft + + TESTE + + + + + + Cadeira de Escritório Preta + + 2 + + 500 + out + + + + + + + + + + + Cadeira de Escritório Preta + + 2 + + 500 + out + + + + + + + + + diff --git a/l10n_br_sale_stock/i18n/l10n_br_sale_stock.pot b/l10n_br_sale_stock/i18n/l10n_br_sale_stock.pot new file mode 100644 index 000000000000..905ae6faa999 --- /dev/null +++ b/l10n_br_sale_stock/i18n/l10n_br_sale_stock.pot @@ -0,0 +1,125 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_br_sale_stock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__button_create_invoice_invisible +msgid "Button Create Invoice Invisible" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: l10n_br_sale_stock +#: model_terms:ir.ui.view,arch_db:l10n_br_sale_stock.l10n_br_sale_stock_res_config_settings_form +msgid "Define if Invoice should be create from Sale Order or Stock Picking" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,help:l10n_br_sale_stock.field_res_company__sale_create_invoice_policy +#: model:ir.model.fields,help:l10n_br_sale_stock.field_res_config_settings__sale_create_invoice_policy +msgid "" +"Define, when Product Type are not service, if Invoice should be create from " +"Sale Order or Stock Picking." +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking__display_name +msgid "Display Name" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking__id +msgid "ID" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking____last_update +msgid "Last Modified on" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__sale_create_invoice_policy +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__sale_create_invoice_policy +msgid "Sale Create Invoice Policy" +msgstr "" + +#. module: l10n_br_sale_stock +#: code:addons/l10n_br_sale_stock/models/res_company.py:0 +#: model:ir.model.fields.selection,name:l10n_br_sale_stock.selection__res_company__sale_create_invoice_policy__sale_order +#, python-format +msgid "Sale Order" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_invoice_onshipping +msgid "Stock Invoice Onshipping" +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: l10n_br_sale_stock +#: code:addons/l10n_br_sale_stock/models/res_company.py:0 +#: model:ir.model.fields.selection,name:l10n_br_sale_stock.selection__res_company__sale_create_invoice_policy__stock_picking +#, python-format +msgid "Stock Picking" +msgstr "" + +#. module: l10n_br_sale_stock +#: model_terms:ir.ui.view,arch_db:l10n_br_sale_stock.l10n_br_sale_stock_res_config_settings_form +msgid "This default value is applied to creation of Invoice." +msgstr "" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/l10n_br_sale_stock/i18n/pt_BR.po b/l10n_br_sale_stock/i18n/pt_BR.po new file mode 100644 index 000000000000..7302af351a9b --- /dev/null +++ b/l10n_br_sale_stock/i18n/pt_BR.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_br_sale_stock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-11-27 22:00+0000\n" +"PO-Revision-Date: 2024-07-29 17:58+0000\n" +"Last-Translator: Marcel Savegnago \n" +"Language-Team: \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__button_create_invoice_invisible +msgid "Button Create Invoice Invisible" +msgstr "Botão Criar fatura invisível" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_res_company +msgid "Companies" +msgstr "Empresas" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de Configurações" + +#. module: l10n_br_sale_stock +#: model_terms:ir.ui.view,arch_db:l10n_br_sale_stock.l10n_br_sale_stock_res_config_settings_form +msgid "Define if Invoice should be create from Sale Order or Stock Picking" +msgstr "" +"Define se a fatura deve ser criada a partir do Pedido de Venda ou da " +"Expedição de Estoque" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,help:l10n_br_sale_stock.field_res_company__sale_create_invoice_policy +#: model:ir.model.fields,help:l10n_br_sale_stock.field_res_config_settings__sale_create_invoice_policy +msgid "" +"Define, when Product Type are not service, if Invoice should be create from " +"Sale Order or Stock Picking." +msgstr "" +"Define, quando o tipo de produto não for serviço, se a fatura deve ser " +"criada a partir do Pedido de Venda ou da Expedição de Estoque." + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move__display_name +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking__display_name +msgid "Display Name" +msgstr "Nome Exibido" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move__id +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking__id +msgid "ID" +msgstr "ID" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_sale_order_line____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_invoice_onshipping____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_move____last_update +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_stock_picking____last_update +msgid "Last Modified on" +msgstr "Última Modificação em" + +#. module: l10n_br_sale_stock +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_company__sale_create_invoice_policy +#: model:ir.model.fields,field_description:l10n_br_sale_stock.field_res_config_settings__sale_create_invoice_policy +msgid "Sale Create Invoice Policy" +msgstr "Política de Criação de Faturas de Venda" + +#. module: l10n_br_sale_stock +#: code:addons/l10n_br_sale_stock/models/res_company.py:0 +#: model:ir.model.fields.selection,name:l10n_br_sale_stock.selection__res_company__sale_create_invoice_policy__sale_order +#, python-format +msgid "Sale Order" +msgstr "Pedido de Venda" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_sale_order +msgid "Sales Order" +msgstr "Pedido de Vendas" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_sale_order_line +msgid "Sales Order Line" +msgstr "Linha do Pedido de Vendas" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_invoice_onshipping +msgid "Stock Invoice Onshipping" +msgstr "Faturamento ao Enviar" + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_move +msgid "Stock Move" +msgstr "Movimento de Estoque" + +#. module: l10n_br_sale_stock +#: code:addons/l10n_br_sale_stock/models/res_company.py:0 +#: model:ir.model.fields.selection,name:l10n_br_sale_stock.selection__res_company__sale_create_invoice_policy__stock_picking +#, python-format +msgid "Stock Picking" +msgstr "Separação de Estoque" + +#. module: l10n_br_sale_stock +#: model_terms:ir.ui.view,arch_db:l10n_br_sale_stock.l10n_br_sale_stock_res_config_settings_form +msgid "This default value is applied to creation of Invoice." +msgstr "Este valor padrão é aplicado à criação de faturas." + +#. module: l10n_br_sale_stock +#: model:ir.model,name:l10n_br_sale_stock.model_stock_picking +msgid "Transfer" +msgstr "Transferir" + +#~ msgid "Invoice Status" +#~ msgstr "Situação do faturamento" + +#~ msgid "Invoiced" +#~ msgstr "Faturado" + +#~ msgid "" +#~ "Invoiced: an invoice already exists\n" +#~ "To Be Invoiced: need to be invoiced\n" +#~ "Not Applicable: no invoice to create" +#~ msgstr "" +#~ "Faturado: já existe uma fatura\n" +#~ "A ser faturado: precisa ser faturado\n" +#~ "Não Aplicável: não precisa ser faturado" + +#~ msgid "Not Applicable" +#~ msgstr "Não Aplicável" + +#~ msgid "Stock Rule" +#~ msgstr "Regra de estoque" + +#~ msgid "To Be Invoiced" +#~ msgstr "Para faturar" + +#~ msgid "Brazilian Localization Sales and Warehouse" +#~ msgstr "Localização Brasileira - Módulo de Vendas e Estoque" + +#~ msgid "Brazilian Localization for sale_stock_module" +#~ msgstr "Localização Brasileira - Integra os módulos de venda e estoque" diff --git a/l10n_br_sale_stock/models/__init__.py b/l10n_br_sale_stock/models/__init__.py new file mode 100644 index 000000000000..63c99b2542fd --- /dev/null +++ b/l10n_br_sale_stock/models/__init__.py @@ -0,0 +1,6 @@ +from . import sale_order_line +from . import stock_move +from . import sale_order +from . import stock_picking +from . import res_company +from . import res_config_settings diff --git a/l10n_br_sale_stock/models/res_company.py b/l10n_br_sale_stock/models/res_company.py new file mode 100644 index 000000000000..173de610ef21 --- /dev/null +++ b/l10n_br_sale_stock/models/res_company.py @@ -0,0 +1,19 @@ +# Copyright (C) 2021 Akretion +# @author Magno Costa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + sale_create_invoice_policy = fields.Selection( + selection=[ + ("sale_order", _("Sale Order")), + ("stock_picking", _("Stock Picking")), + ], + help="Define, when Product Type are not service, if Invoice" + " should be create from Sale Order or Stock Picking.", + default="stock_picking", + ) diff --git a/l10n_br_sale_stock/models/res_config_settings.py b/l10n_br_sale_stock/models/res_config_settings.py new file mode 100644 index 000000000000..096b630f6854 --- /dev/null +++ b/l10n_br_sale_stock/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Akretion +# @author Magno Costa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sale_create_invoice_policy = fields.Selection( + related="company_id.sale_create_invoice_policy", readonly=False + ) diff --git a/l10n_br_sale_stock/models/sale_order.py b/l10n_br_sale_stock/models/sale_order.py new file mode 100644 index 000000000000..f20e8de1bff6 --- /dev/null +++ b/l10n_br_sale_stock/models/sale_order.py @@ -0,0 +1,64 @@ +# Copyright (C) 2020 Magno Costa - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + # Make Invisible Invoice Button + button_create_invoice_invisible = fields.Boolean( + compute="_compute_get_button_create_invoice_invisible" + ) + + @api.depends("state", "order_line.invoice_status") + def _compute_get_button_create_invoice_invisible(self): + button_create_invoice_invisible = False + + lines = self.order_line.filtered( + lambda line: line.invoice_status == "to invoice" + ) + + # Somente depois do Pedido confirmado o botão pode aparecer + if self.state != "sale": + button_create_invoice_invisible = True + else: + if self.company_id.sale_create_invoice_policy == "stock_picking": + # A criação de Fatura de Serviços deve ser possível via Pedido + if not any(line.product_id.type == "service" for line in lines): + button_create_invoice_invisible = True + else: + # No caso da Politica de criação baseada no Pedido de Venda + # qdo acionado o Botão irá criar as Faturas automaticamente + # mesmo no caso de ter Produtos e Serviços + if not lines: + button_create_invoice_invisible = True + + self.button_create_invoice_invisible = button_create_invoice_invisible + + @api.onchange("partner_shipping_id") + def _onchange_partner_shipping_id(self): + """ + Caso ocorra a alteração do campo Endereço de Entrega/partner_shipping_id + depois do Pedido confirmado os stock.picking relacionados ficam com o + partner_id anterior, o que é errado, o metodo original apenas mostra uma + mensagem de alerta na tela orientando o usuário a corrigir manualmente, + mas como na localização com esse modulo pode se definir a criação da + Invoice a partir do stock.picking é melhor garantir a alteração do campo + partner_id da stock.picking afim de evitar erros na criação da Invoice. + :return: super() + """ + # TODO: Verificar essa questão na migração a partir da v14 + + pickings = self.picking_ids.filtered( + lambda p: p.state not in ["done", "cancel"] + and p.partner_id != self.partner_shipping_id + ) + # Atribuição da forma abaixo por algum motivo + # não funciona apenas o write + # for picking in pickings: + # picking.partner_id = self.partner_shipping_id + pickings.write({"partner_id": self.partner_shipping_id.id}) + + return super()._onchange_partner_shipping_id() diff --git a/l10n_br_sale_stock/models/sale_order_line.py b/l10n_br_sale_stock/models/sale_order_line.py new file mode 100644 index 000000000000..60fa69291807 --- /dev/null +++ b/l10n_br_sale_stock/models/sale_order_line.py @@ -0,0 +1,58 @@ +# Copyright (C) 2013 Raphaël Valyi - Akretion +# Copyright (C) 2014 Renato Lima - Akretion +# Copyright (C) 2021 Magno Costa - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _prepare_procurement_values(self, group_id=False): + values = {} + if self.order_id.fiscal_operation_id: + values = self._prepare_br_fiscal_dict() + values.update(super()._prepare_procurement_values(group_id)) + # Incluir o invoice_state + if self.order_id.company_id.sale_create_invoice_policy == "stock_picking": + values["invoice_state"] = "2binvoiced" + + return values + + # no trigger product_id.invoice_policy to avoid retroactively changing SO + @api.depends("qty_invoiced", "qty_delivered", "product_uom_qty", "state") + def _compute_qty_to_invoice(self): + """ + Compute the quantity to invoice. If the invoice policy is order, + the quantity to invoice is calculated from the ordered quantity. + Otherwise, the quantity delivered is used. + """ + result = super()._compute_qty_to_invoice() + + for line in self: + if line.order_id.state in ["sale", "done"]: + if line.state in ["sale", "done"] and not line.display_type: + if ( + line.order_id.company_id.sale_create_invoice_policy + == "stock_picking" + and line.product_id.type == "product" + ): + # O correto seria que ao selecionar + # sale_create_invoice_policy 'stock_picking' os + # produtos tenham o campo invoice_policy definidos para + # 'delivery', porém para evitar que seja criada uma + # Fatura a partir do Pedido de Venda estamos + # alterando isso mesmo para os produtos definidos com + # 'order', já que a Politica de Criação da Fatura no + # caso do Tipo Produto está definida para ser a + # partir do stock.picking . + # TODO: Essa seria a melhor opção ? Por enquanto pelo + # que vi para ter o mesmo resultado, que é no caso + # sale_create_invoice_policy 'stock_picking' só ser + # possível criar a partir do sale.order Faturas das + # linhas que sejam type service sim, a outra opção + # seria sobre escrever o metodo action_invoice_create + # sem ser possível chamar o super. + line.qty_to_invoice = 0 + return result diff --git a/l10n_br_sale_stock/models/stock_move.py b/l10n_br_sale_stock/models/stock_move.py new file mode 100644 index 000000000000..f3b03160f8d1 --- /dev/null +++ b/l10n_br_sale_stock/models/stock_move.py @@ -0,0 +1,51 @@ +# Copyright (C) 2020 Gabriel Cardoso de Faria +# Copyright (C) 2021 Magno Costa - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _get_price_unit_invoice(self, inv_type, partner, qty=1): + result = super()._get_price_unit_invoice(inv_type, partner, qty) + # Caso tenha Sale Line já vem desagrupado aqui devido ao KEY + if len(self) == 1: + # Caso venha apenas uma linha porem sem + # sale_line_id é preciso ignora-la + if self.sale_line_id and self.sale_line_id.price_unit != result: + result = self.sale_line_id.price_unit + + return result + + def _get_new_picking_values(self): + # IMPORTANTE: a sequencia de update dos dicionarios quando o + # partner_shipping_id é diferente, o metodo do fiscal está + # sobre escrevendo o partner_id e acaba criando um picking + # sem o partner_id caso esse dict atualize o do super + values = {} + fiscal_operation = False + if self.sale_line_id: + if self.sale_line_id.order_id.fiscal_operation_id: + fiscal_operation = self.sale_line_id.order_id.fiscal_operation_id + values = self.sale_line_id.order_id._prepare_br_fiscal_dict() + + values.update(super()._get_new_picking_values()) + # self is a recordset, possibly with different fiscal operations + # so we use the fiscal_opration from the SO for the picking: + if fiscal_operation: + values.update({"fiscal_operation_id": fiscal_operation.id}) + + return values + + def _get_fiscal_partner(self): + """ + Use partner_invoice_id if different from partner + """ + self.ensure_one() + partner = super()._get_fiscal_partner() + if self.sale_line_id: + if partner != self.sale_line_id.order_id.partner_invoice_id: + partner = self.sale_line_id.order_id.partner_invoice_id + return partner diff --git a/l10n_br_sale_stock/models/stock_picking.py b/l10n_br_sale_stock/models/stock_picking.py new file mode 100644 index 000000000000..30641d141ee1 --- /dev/null +++ b/l10n_br_sale_stock/models/stock_picking.py @@ -0,0 +1,43 @@ +# Copyright (C) 2021 Magno Costa - Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _get_partner_to_invoice(self): + """ + If the partner has some invoicing contact defined + partner_invoice_id is auto filled, but it can also be changed. + partner_invoice_id is used if different from partner_id + """ + self.ensure_one() + partner_id = super()._get_partner_to_invoice() + if self.sale_id: + if partner_id != self.sale_id.partner_invoice_id.id: + partner_id = self.sale_id.partner_invoice_id.id + return partner_id + + def _get_fiscal_partner(self): + self.ensure_one() + partner = super()._get_fiscal_partner() + partner_to_invoice = self._get_partner_to_invoice() + if partner.id != partner_to_invoice: + partner = self.env["res.partner"].browse(partner_to_invoice) + return partner + + def _get_default_fiscal_operation(self): + fiscal_operation = super()._get_default_fiscal_operation() + if self.sale_id: + if self.sale_id.fiscal_operation_id: + # Evita a inconsistência de ter o Pedido de Vendas com uma + # OP Fiscal e a Ordem de Seleção outra, quando o campo + # invoice_state é alterado, o usuário pode alterar o campo + # mas dessa forma forçamos a decisão de não usar a mesma + # do Pedido. + if fiscal_operation != self.sale_id.fiscal_operation_id: + fiscal_operation = self.sale_id.fiscal_operation_id + + return fiscal_operation diff --git a/l10n_br_sale_stock/pyproject.toml b/l10n_br_sale_stock/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/l10n_br_sale_stock/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_br_sale_stock/readme/CONFIGURE.md b/l10n_br_sale_stock/readme/CONFIGURE.md new file mode 100644 index 000000000000..e7dc235973ab --- /dev/null +++ b/l10n_br_sale_stock/readme/CONFIGURE.md @@ -0,0 +1 @@ +No configuration required. diff --git a/l10n_br_sale_stock/readme/CONTRIBUTORS.md b/l10n_br_sale_stock/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..9b43cc4a7777 --- /dev/null +++ b/l10n_br_sale_stock/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Renato Lima \<\> +- Raphaël Valyi \<\> +- Magno Costa \<\> +- Gabriel Cardoso de Faria \<\> diff --git a/l10n_br_sale_stock/readme/CREDITS.md b/l10n_br_sale_stock/readme/CREDITS.md new file mode 100644 index 000000000000..1acc1610318e --- /dev/null +++ b/l10n_br_sale_stock/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- Aketion LTDA diff --git a/l10n_br_sale_stock/readme/DESCRIPTION.md b/l10n_br_sale_stock/readme/DESCRIPTION.md new file mode 100644 index 000000000000..de38745dc08c --- /dev/null +++ b/l10n_br_sale_stock/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +Este módulo estende o módulo padrão de vendas e estoque do Odoo para +atender às necessidades específicas do Brasil, especialmente no contexto +de vendas de produtos com NF-e (Nota Fiscal Eletrônica). + +Ele automatiza a propagação da operação fiscal, comentários fiscais e +incoterms para as faturas geradas diretamente a partir de ordens de +venda ou movimentações de estoque, garantindo que essas informações +sejam corretamente transferidas e utilizadas no processo de faturamento. diff --git a/l10n_br_sale_stock/readme/HISTORY.md b/l10n_br_sale_stock/readme/HISTORY.md new file mode 100644 index 000000000000..e430bd123234 --- /dev/null +++ b/l10n_br_sale_stock/readme/HISTORY.md @@ -0,0 +1,11 @@ +## 15.0.1.0.0 (2024-09-17) + +- \[MIG\] Migration to version 15.0 + +## 12.0.1.0.0 (2020-05-29) + +- \[MIG\] Migration to version 12.0 + +## 10.0.1.0.0 (2019-09-13) + +- \[MIG\] Migration to version 10.0 diff --git a/l10n_br_sale_stock/readme/INSTALL.md b/l10n_br_sale_stock/readme/INSTALL.md new file mode 100644 index 000000000000..6730ca137eec --- /dev/null +++ b/l10n_br_sale_stock/readme/INSTALL.md @@ -0,0 +1,5 @@ +This module depends on: + +- sale_stock +- l10n_br_sale +- l10n_br_stock_account diff --git a/l10n_br_sale_stock/readme/ROADMAP.md b/l10n_br_sale_stock/readme/ROADMAP.md new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/l10n_br_sale_stock/readme/ROADMAP.md @@ -0,0 +1 @@ + diff --git a/l10n_br_sale_stock/readme/USAGE.md b/l10n_br_sale_stock/readme/USAGE.md new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/l10n_br_sale_stock/readme/USAGE.md @@ -0,0 +1 @@ + diff --git a/l10n_br_sale_stock/static/description/icon.png b/l10n_br_sale_stock/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/l10n_br_sale_stock/static/description/icon.png differ diff --git a/l10n_br_sale_stock/static/description/index.html b/l10n_br_sale_stock/static/description/index.html new file mode 100644 index 000000000000..9317dad5073f --- /dev/null +++ b/l10n_br_sale_stock/static/description/index.html @@ -0,0 +1,492 @@ + + + + + +Brazilian Localization Sales and Warehouse + + + +
+

Brazilian Localization Sales and Warehouse

+ + +

Beta License: AGPL-3 OCA/l10n-brazil Translate me on Weblate Try me on Runboat

+

Este módulo estende o módulo padrão de vendas e estoque do Odoo para +atender às necessidades específicas do Brasil, especialmente no contexto +de vendas de produtos com NF-e (Nota Fiscal Eletrônica).

+

Ele automatiza a propagação da operação fiscal, comentários fiscais e +incoterms para as faturas geradas diretamente a partir de ordens de +venda ou movimentações de estoque, garantindo que essas informações +sejam corretamente transferidas e utilizadas no processo de faturamento.

+

Table of contents

+ +
+

Installation

+

This module depends on:

+
    +
  • sale_stock
  • +
  • l10n_br_sale
  • +
  • l10n_br_stock_account
  • +
+
+
+

Configuration

+

No configuration required.

+
+
+

Usage

+
+ +
+

Changelog

+
+

15.0.1.0.0 (2024-09-17)

+
    +
  • [MIG] Migration to version 15.0
  • +
+
+
+

12.0.1.0.0 (2020-05-29)

+
    +
  • [MIG] Migration to version 12.0
  • +
+
+
+

10.0.1.0.0 (2019-09-13)

+
    +
  • [MIG] Migration to version 10.0
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Aketion LTDA
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

renatonlima mbcosta

+

This module is part of the OCA/l10n-brazil project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/l10n_br_sale_stock/tests/__init__.py b/l10n_br_sale_stock/tests/__init__.py new file mode 100644 index 000000000000..a64b0d449d2f --- /dev/null +++ b/l10n_br_sale_stock/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_stock diff --git a/l10n_br_sale_stock/tests/test_sale_stock.py b/l10n_br_sale_stock/tests/test_sale_stock.py new file mode 100644 index 000000000000..d1173cbdf37b --- /dev/null +++ b/l10n_br_sale_stock/tests/test_sale_stock.py @@ -0,0 +1,527 @@ +# Copyright 2020 KMEE +# Copyright (C) 2021 Magno Costa - Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +# TODO: In v16 check the possiblity to use the commom.py +# from stock_picking_invoicing +# https://github.com/OCA/account-invoicing/blob/16.0/ +# stock_picking_invoicing/tests/common.py +from odoo.tests import Form, tagged + +from odoo.addons.l10n_br_stock_account.tests.common import TestBrPickingInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestSaleStock(TestBrPickingInvoicingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_02_sale_stock_return(self): + """ + Test a SO with a product invoiced on delivery. Deliver and invoice + the SO, then do a return + of the picking. Check that a refund invoice is well generated. + """ + # intial so + self.partner = self.env.ref("l10n_br_base.res_partner_address_ak2") + self.product = self.env.ref("product.product_delivery_01") + so_vals = { + "partner_id": self.partner.id, + "partner_invoice_id": self.partner.id, + "partner_shipping_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "product_uom_qty": 3.0, + "product_uom": self.product.uom_id.id, + "price_unit": self.product.list_price, + }, + ) + ], + "pricelist_id": self.env.ref("product.list0").id, + } + self.so = self.env["sale.order"].create(so_vals) + + for line in self.so.order_line: + line._onchange_product_id_fiscal() + + # confirm our standard so, check the picking + self.so.action_confirm() + self.assertTrue( + self.so.picking_ids, + 'Sale Stock: no picking created for "invoice on ' + 'delivery" storable products', + ) + + # set stock.picking to be invoiced + self.assertTrue( + len(self.so.picking_ids) == 1, + "More than one stock " "picking for sale.order", + ) + self.so.picking_ids.set_to_be_invoiced() + + # validate stock.picking + stock_picking = self.so.picking_ids + + # compare sale.order.line with stock.move + stock_move = stock_picking.move_ids + sale_order_line = self.so.order_line + + sm_fields = [key for key in self.env["stock.move"]._fields.keys()] + sol_fields = [key for key in self.env["sale.order.line"]._fields.keys()] + + skipped_fields = [ + "id", + "display_name", + "state", + ] + common_fields = list(set(sm_fields) & set(sol_fields) - set(skipped_fields)) + + for field in common_fields: + self.assertEqual( + stock_move[field], + sale_order_line[field], + "Field %s failed to transfer from " + "sale.order.line to stock.move" % field, + ) + + self.env["stock.immediate.transfer"].create( + {"pick_ids": [(4, stock_picking.id)]} + ).process() + + # O valor do price_unit da stock.move é alterado ao Confirmar o + # stock.picking de acordo com a forma de valorização de estoque + # definida( ex.: metodo _run_fifo é chamado e altera o valor do + # price_unit https://github.com/odoo/odoo/blob/12.0/addons + # /stock_account/models/stock.py#L255 ), por isso os campos + # relacionados a esse valor não são iguais. + # O teste está sendo feito novamente para essa questão ficar clara + # em alterações e migrações. + skipped_fields_after_confirm = [ + "price_gross", + "amount_taxed", + "financial_total", + "financial_total_gross", + "fiscal_price", + "amount_fiscal", + "price_unit", + "amount_untaxed", + "amount_total", + ] + skipped_fields[len(skipped_fields) :] = skipped_fields_after_confirm + + common_fields = list(set(sm_fields) & set(sol_fields) - set(skipped_fields)) + + for field in common_fields: + self.assertEqual( + stock_move[field], + sale_order_line[field], + "Field %s failed to transfer from " + "sale.order.line to stock.move" % field, + ) + + def test_picking_sale_order_product_and_service(self): + """ + Test Sale Order with product and service + """ + + sale_order_2 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_2") + sale_order_2.action_confirm() + + # Forma encontrada para chamar o metodo + # _compute_get_button_create_invoice_invisible + sale_order_form = Form(sale_order_2) + sale_order = sale_order_form.save() + # Metodo de criação da fatura a partir do sale.order + # deve gerar apenas a linha de serviço + sale_order._create_invoices(final=True) + # Deve existir apenas a Fatura/Documento Fiscal de Serviço + self.assertEqual(1, sale_order.invoice_count) + for invoice in sale_order.invoice_ids: + for line in invoice.invoice_line_ids: + self.assertEqual(line.product_id.type, "service") + # Confirmando a Fatura de Serviço + invoice.action_post() + self.assertEqual( + invoice.state, "posted", "Invoice should be in state Posted." + ) + + picking = sale_order.picking_ids + # Check product availability + picking.action_assign() + # Apenas o Produto criado + self.assertEqual(len(picking.move_ids_without_package), 1) + self.assertEqual(picking.invoice_state, "2binvoiced") + # Force product availability + for move in picking.move_ids_without_package: + move.quantity_done = move.product_uom_qty + # Usado para validar a transferencia dos campos da linha + # do Pedido de Venda para a linha da Fatura/Invoice + sale_order_line = move.sale_line_id + self.assertEqual(sale_order_line.product_uom, move.product_uom) + + self.picking_move_state(picking) + self.assertEqual(picking.state, "done") + invoice = self.create_invoice_wizard(picking) + self.assertEqual(picking.invoice_state, "invoiced") + self.assertIn(invoice, picking.invoice_ids) + self.assertIn(picking, invoice.picking_ids) + # Picking criado com o Partner Shipping da Sale Order + self.assertEqual(picking.partner_id, sale_order_2.partner_shipping_id) + # Fatura criada com o Partner Invoice da Sale Order + self.assertEqual(invoice.partner_id, sale_order_2.partner_invoice_id) + # Fatura criada com o Partner Shipping usado no Picking + self.assertEqual(invoice.partner_shipping_id, picking.partner_id) + # Quando informado usar o Termo de Pagto definido no Pedido de Venda + # e não o padrão do cliente + self.assertEqual(invoice.invoice_payment_term_id, sale_order_2.payment_term_id) + + # Apenas a Fatura com a linha do produto foi criada + self.assertEqual(len(invoice.invoice_line_ids), 1) + + # No Pedido de Venda devem existir duas Faturas/Documentos Fiscais + # de Serviço e Produto + self.assertEqual(2, sale_order_2.invoice_count) + + # Confirmando a Fatura + invoice.action_post() + self.assertEqual(invoice.state, "posted", "Invoice should be in state Posted.") + + # Validar Atualização da Quantidade Faturada + for line in sale_order_2.order_line: + # Apenas a linha de Produto tem a qtd faturada dobrada + if line.product_id.type == "product": + # A quantidade Faturada deve ser igual + # a Quantidade do Produto + self.assertEqual(line.product_uom_qty, line.qty_invoiced) + + # Checar se os campos das linhas do Pedido de Vendas + # estão iguais as linhas da Fatura/Invoice. + sol_fields = [key for key in self.env["sale.order.line"]._fields.keys()] + + acl_fields = [key for key in self.env["account.move.line"]._fields.keys()] + + skipped_fields = [ + "agent_ids", + "id", + "display_name", + "state", + "create_date", + # O campo da Unidade de Medida possui um nome diferente na + # account.move.line product_uom_id, por isso é removido porém + # a copia entre os objetos é testada tanto no stock.move acima + # quanto na account.move.line abaixo + "uom_id", + # O campo 'display_type' difere entre 'sale.order.line' e + # 'account.move.line' para produtos: é False em 'sale.order.line' e + # "product" em 'account.move.line' + "display_type", + "sequence", + "analytic_precision", # verificar se tem importancia + # Ao chamar o _onchange_product_id_fiscal no stock.move o + # partner_id usado no mapeamento é o do objeto, nesse teste + # 'Akretion Aluminio - SP' por ser o Endereço de Entrega + # partner_shipping_id, porém esse não é o partner_invoice_id + # 'Akretion Sao Paulo' essa diferença ocasiona diferentes + # 'Linhas de Operações Fiscal'/fiscal_operation_line_id entre: + # Objeto | Linha de Operações Fiscal + # _______________________________|____________________________ + # sale.order.line | 'Revenda não Contribuinte' + # stock.move e account.move.line | 'Revenda' + # TODO: O mapeamento da 'Linha de Operações Fiscal' precisa + # considerar os casos onde o partner_id do objeto não é o + # partner_invoice_id. Por enquanto o campo não está sendo validado + # para evitar erros aqui já que isso precisa ser resolvido em outro + # modulo ou talvez aqui porém seria apenas uma correção temporaria. + "fiscal_operation_line_id", + ] + + common_fields = list(set(acl_fields) & set(sol_fields) - set(skipped_fields)) + invoice_lines = picking.invoice_ids.invoice_line_ids + + for field in common_fields: + self.assertEqual( + sale_order_line[field], + invoice_lines[field], + "Field %s failed to transfer from " + "sale.order.line to account.move.line" % field, + ) + # Assert específico para 'display_type' + if sale_order_line.display_type is False: + self.assertEqual( + invoice_lines.display_type, + "product", + "Esperado 'display_type' como 'product' em account.move.line quando" + "é False em sale.order.line", + ) + + for inv_line in invoice_lines: + if inv_line.product_id == sale_order_line.product_id: + self.assertEqual(sale_order_line.product_uom, inv_line.product_uom_id) + + # Teste de Retorno + picking_devolution = self.return_picking_wizard(picking) + + self.assertEqual(picking_devolution.invoice_state, "2binvoiced") + self.assertTrue( + picking_devolution.fiscal_operation_id, "Missing Fiscal Operation." + ) + for line in picking_devolution.move_ids: + self.assertEqual(line.invoice_state, "2binvoiced") + # Valida presença dos campos principais para o mapeamento Fiscal + self.assertTrue(line.fiscal_operation_id, "Missing Fiscal Operation.") + self.assertTrue( + line.fiscal_operation_line_id, "Missing Fiscal Operation Line." + ) + + self.picking_move_state(picking_devolution) + self.assertEqual(picking_devolution.state, "done", "Change state fail.") + invoice_devolution = self.create_invoice_wizard(picking_devolution) + + # Confirmando a Fatura + invoice_devolution.action_post() + self.assertEqual( + invoice_devolution.state, "posted", "Invoice should be in state Posted." + ) + # Validar Atualização da Quantidade Faturada + for line in sale_order_2.order_line: + # Apenas a linha de Produto tem a qtd faturada dobrada + if line.product_id.type == "product": + # A quantidade Faturada deve ser zero + # devido a Devolução + self.assertEqual(0.0, line.qty_invoiced) + + def test_picking_invoicing_partner_shipping_invoiced(self): + """ + Test the invoice generation grouped by partner/product with 2 + picking and 3 moves per picking, but Partner to Shipping is + different from Partner to Invoice. + """ + sale_order_1 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_1") + sale_order_1.action_confirm() + picking = sale_order_1.picking_ids + self.picking_move_state(picking) + + sale_order_2 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_2") + sale_order_2.action_confirm() + picking2 = sale_order_2.picking_ids + + self.picking_move_state(picking2) + self.assertEqual(picking.state, "done") + self.assertEqual(picking2.state, "done") + pickings = picking | picking2 + invoice = self.create_invoice_wizard(pickings) + + # Fatura Agrupada + self.assertEqual(len(invoice), 1) + self.assertEqual(picking.invoice_state, "invoiced") + self.assertEqual(picking2.invoice_state, "invoiced") + # Fatura deverá ser criada com o partner_invoice_id + self.assertEqual(invoice.partner_id, sale_order_1.partner_invoice_id) + # Fatura com o partner shipping + self.assertEqual(invoice.partner_shipping_id, picking.partner_id) + self.assertIn(invoice, picking.invoice_ids) + self.assertIn(picking, invoice.picking_ids) + self.assertIn(invoice, picking2.invoice_ids) + self.assertIn(picking2, invoice.picking_ids) + + # Not grouping products with different sale line, + # 3 products from sale_order_1 and 1 product from sale_order_2 + self.assertEqual(len(invoice.invoice_line_ids), 4) + for inv_line in invoice.invoice_line_ids: + # TODO: No travis falha o browse aqui + # l10n_br_stock_account/models/stock_invoice_onshipping.py:105 + # isso não acontece no caso da empresa de Lucro Presumido + # ou quando é feito o teste apenas instalando os modulos + # l10n_br_account e em seguida o l10n_br_stock_account + # self.assertTrue( + # inv_line.tax_ids, "Error to map Sale Tax in invoice.line." + # ) + # Valida presença dos campos principais para o mapeamento Fiscal + self.assertTrue(inv_line.fiscal_operation_id, "Missing Fiscal Operation.") + self.assertTrue( + inv_line.fiscal_operation_line_id, "Missing Fiscal Operation Line." + ) + + def test_ungrouping_pickings_partner_shipping_different(self): + """ + Test the invoice generation grouped by partner/product with 3 + picking and 3 moves per picking, the 3 has the same Partner to + Invoice but one has Partner to Shipping so shouldn't be grouping. + """ + sale_order_1 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_1") + sale_order_1.action_confirm() + picking = sale_order_1.picking_ids + self.picking_move_state(picking) + + sale_order_3 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_3") + sale_order_3.action_confirm() + picking3 = sale_order_3.picking_ids + self.picking_move_state(picking3) + self.assertEqual(picking.state, "done") + self.assertEqual(picking3.state, "done") + + sale_order_4 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_4") + sale_order_4.action_confirm() + picking4 = sale_order_4.picking_ids + self.picking_move_state(picking4) + self.assertEqual(picking.state, "done") + self.assertEqual(picking3.state, "done") + + pickings = picking | picking3 | picking4 + invoices = self.create_invoice_wizard(pickings) + + # Mesmo tendo o mesmo Partner Invoice se não tiver o + # mesmo Partner Shipping não deve ser Agrupado + self.assertEqual(len(invoices), 2) + self.assertEqual(picking.invoice_state, "invoiced") + self.assertEqual(picking3.invoice_state, "invoiced") + self.assertEqual(picking4.invoice_state, "invoiced") + + # Fatura que tem um Partner shipping + # diferente não foi agrupada + invoice_pick_1 = invoices.filtered( + lambda t: t.partner_shipping_id == picking.partner_id + ) + # Fatura deverá ser criada com o partner_invoice_id + self.assertEqual(invoice_pick_1.partner_id, sale_order_1.partner_invoice_id) + # Fatura criada com o Partner Shipping usado no Picking + self.assertEqual(invoice_pick_1.partner_shipping_id, picking.partner_id) + + # TODO: O processo de criação a partir de um Pedido de Venda vem + # preenchido o campo partner_shipping_id, isso deve ser mantido por + # ser considerado o padrão ou é melhor remover o partner_shipping_id + # quando o valor é igual ao partner_id? + + # Fatura Agrupada, não deve ter o partner_shipping_id preenchido + # invoice_pick_3_4 = invoices.filtered(lambda t: not t.partner_shipping_id) + + invoice_pick_3_4 = invoices.filtered( + lambda t: t.partner_shipping_id == t.partner_id + ) + self.assertIn(invoice_pick_3_4, picking3.invoice_ids) + self.assertIn(invoice_pick_3_4, picking4.invoice_ids) + + def test_synchronize_sale_partner_shipping_in_stock_picking(self): + """ + Test the synchronize Sale Partner Shipping in Stock Picking + """ + sale_order_1 = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_1") + sale_order_1.action_confirm() + picking = sale_order_1.picking_ids + sale_order_1.partner_shipping_id = self.env.ref( + "l10n_br_base.res_partner_address_ak2" + ).id + sale_order_1._onchange_partner_shipping_id() + self.assertEqual(sale_order_1.partner_shipping_id, picking.partner_id) + + def test_lucro_presumido_company(self): + """ + Test Lucro Presumido Company + """ + self._change_user_company(self.env.ref("l10n_br_base.empresa_lucro_presumido")) + sale_order_1 = self.env.ref( + "l10n_br_sale_stock.l10n_br_sale_stock_lucro_presumido" + ) + sale_order_form = Form(sale_order_1) + sale_order = sale_order_form.save() + sale_order.incoterm = self.env.ref("account.incoterm_FOB") + + sale_order.action_confirm() + picking = sale_order_1.picking_ids + self.picking_move_state(picking) + invoice = self.create_invoice_wizard(picking) + self.assertEqual(len(invoice), 1) + for inv_line in invoice.invoice_line_ids: + # TODO: No Travis quando a empresa main_company falha esse browse aqui + # l10n_br_stock_account/models/stock_invoice_onshipping.py:105 + # isso não acontece no caso da empresa de Lucro Presumido ou quando é + # feito o teste apenas instalando os modulos l10n_br_account e em + # seguida o l10n_br_stock_account. + self.assertTrue(inv_line.tax_ids, "Error to map Sale Tax in invoice.line.") + + def test_button_create_bill_in_view(self): + """ + Test Field to make Button Create Bill invisible. + """ + sale_order_form = Form(self.env.ref("l10n_br_sale.main_so_only_products")) + sale_products = sale_order_form.save() + # Caso do Pedido de Vendas em Rascunho + self.assertTrue( + sale_products.button_create_invoice_invisible, + "Field to make invisible the Button Create Bill should be" + " invisible when Sale Order is not in state Sale or Done.", + ) + sale_products.action_confirm() + self.assertTrue( + sale_products.button_create_invoice_invisible, + "Field to make invisible the button Create Bill should be" + " invisible when Sale Order has only products.", + ) + + # Caso somente Serviços + sale_order_form = Form(self.env.ref("l10n_br_sale.main_so_only_services")) + sale_only_service = sale_order_form.save() + sale_only_service.action_confirm() + self.assertFalse( + sale_only_service.button_create_invoice_invisible, + "Field to make invisible the Button Create Bill should be" + " False when the Sale Order has only Services.", + ) + + # Caso Produto e Serviço + sale_order_form = Form(self.env.ref("l10n_br_sale.main_so_product_service")) + sale_service_product = sale_order_form.save() + sale_service_product.action_confirm() + self.assertFalse( + sale_only_service.button_create_invoice_invisible, + "Field to make invisible the Button Create Bill should be" + " False when the Sale Order has Service and Product.", + ) + + def test_compatible_with_international_case(self): + """ + Test compatibility with international cases or + without Fiscal Operation. + """ + so_international = self.env.ref("sale.sale_order_3") + so_international.fiscal_operation_id = False + so_international.action_confirm() + picking = so_international.picking_ids + self.picking_move_state(picking) + invoice = self.create_invoice_wizard(picking) + invoice.action_post() + # Caso Internacional não deve ter Documento Fiscal associado + self.assertFalse( + invoice.fiscal_document_id, + "International case should not has Fiscal Document.", + ) + # Teste Retorno + picking_devolution = self.return_picking_wizard(picking) + invoice_devolution = self.create_invoice_wizard(picking_devolution) + self.assertFalse( + invoice_devolution.fiscal_document_id, + "International case should not has Fiscal Document.", + ) + + def test_form_stock_picking(self): + """Test Stock Picking with Form""" + + sale_order = self.env.ref("l10n_br_sale_stock.main_so_l10n_br_sale_stock_1") + sale_order.action_confirm() + picking = sale_order.picking_ids + self.picking_move_state(picking) + picking_form = Form(picking) + + # Apesar do metodo onchange retornar uma OP Fiscal padrão, + # quando existe um Pedido de Venda associado deve usar retornar + # a mesma OP Fiscal do Pedido. + picking_form.invoice_state = "none" + picking_form.invoice_state = "2binvoiced" + self.assertEqual(sale_order.fiscal_operation_id, picking.fiscal_operation_id) + picking_form.save() diff --git a/l10n_br_sale_stock/views/res_company_view.xml b/l10n_br_sale_stock/views/res_company_view.xml new file mode 100644 index 000000000000..745c0dc121bf --- /dev/null +++ b/l10n_br_sale_stock/views/res_company_view.xml @@ -0,0 +1,15 @@ + + + + + l10n_br_fiscal.res.company.form + res.company + + + + + + + + + diff --git a/l10n_br_sale_stock/views/res_config_settings_view.xml b/l10n_br_sale_stock/views/res_config_settings_view.xml new file mode 100644 index 000000000000..c6f4be48273d --- /dev/null +++ b/l10n_br_sale_stock/views/res_config_settings_view.xml @@ -0,0 +1,38 @@ + + + + + l10n_br_sale_stock.res.config.settings.form + res.config.settings + + + +
+
+
+
+
+
+
+ +
diff --git a/l10n_br_sale_stock/views/sale_order_view.xml b/l10n_br_sale_stock/views/sale_order_view.xml new file mode 100644 index 000000000000..33829f3bac45 --- /dev/null +++ b/l10n_br_sale_stock/views/sale_order_view.xml @@ -0,0 +1,38 @@ + + + + + l10n_br_sale_stock.order.form + sale.order + + 99 + + + + + + {'invisible': [('button_create_invoice_invisible', '=', True)]} + + + + {'invisible': ['|', ('button_create_invoice_invisible', '=', True), ('state', '=', 'sale')]} + + + + + diff --git a/l10n_br_sale_stock/wizards/__init__.py b/l10n_br_sale_stock/wizards/__init__.py new file mode 100644 index 000000000000..87b9317d657c --- /dev/null +++ b/l10n_br_sale_stock/wizards/__init__.py @@ -0,0 +1 @@ +from . import stock_invoice_onshipping diff --git a/l10n_br_sale_stock/wizards/stock_invoice_onshipping.py b/l10n_br_sale_stock/wizards/stock_invoice_onshipping.py new file mode 100644 index 000000000000..e09e86eecb38 --- /dev/null +++ b/l10n_br_sale_stock/wizards/stock_invoice_onshipping.py @@ -0,0 +1,108 @@ +# Copyright 2020 KMEE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class StockInvoiceOnshipping(models.TransientModel): + _inherit = "stock.invoice.onshipping" + + def _build_invoice_values_from_pickings(self, pickings): + """ + Build dict to create a new invoice from given pickings + :param pickings: stock.picking recordset + :return: dict + """ + invoice, values = super()._build_invoice_values_from_pickings(pickings) + + pick = fields.first(pickings) + if pick.sale_id: + values.update( + { + "partner_id": pick.sale_id.partner_invoice_id.id, + } + ) + + if pick.sale_id.payment_term_id.id != values.get("invoice_payment_term_id"): + values.update( + {"invoice_payment_term_id": pick.sale_id.payment_term_id.id} + ) + + # O campo payment_mode_id é implementado com a instalação do + # l10n_br_account_nfe mas o l10n_br_sale_stock não tem + # dependencia direta desse modulo, para evitar a necessidade + # de um 'glue' modulo para resolver isso é feita a verificação + # se o campo existe antes de preenche-lo + if hasattr(pick.sale_id, "payment_mode_id"): + if pick.sale_id.payment_mode_id.id != values.get("payment_mode_id"): + values.update({"payment_mode_id": pick.sale_id.payment_mode_id.id}) + if pick.sale_id.incoterm.id != values.get("invoice_incoterm_id"): + values.update({"invoice_incoterm_id": pick.sale_id.incoterm.id}) + + if pick.sale_id.copy_note and pick.sale_id.note: + # Evita enviar False quando não tem nada + additional_data = "" + if pick.sale_id.manual_customer_additional_data: + additional_data = f"{pick.sale_id.manual_customer_additional_data}" + + values.update( + { + "manual_customer_additional_data": additional_data + + f" TERMOS E CONDIÇÕES: {pick.sale_id.note}", + } + ) + + return invoice, values + + def _get_move_key(self, move): + """ + Get the key based on the given move + :param move: stock.move recordset + :return: key + """ + key = super()._get_move_key(move) + if move.sale_line_id: + # Apesar da linha da Fatura permitir ter mais de uma linha de + # pedido de venda associada(campo sale_line_ids na invoice line) + # existe um erro a ser resolvido + # Issue https://github.com/odoo/odoo/issues/77028 + # PR https://github.com/odoo/odoo/pull/77195 + # Além disso é preciso verificar outras questões + # por exemplo datas de entrega diferentes, informações + # comerciais que são discriminadas por itens e etc. + key = key + (move.sale_line_id,) + + return key + + def _get_invoice_line_values(self, moves, invoice_values, invoice): + """ + Create invoice line values from given moves + :param moves: stock.move + :param invoice: account.invoice + :return: dict + """ + + values = super()._get_invoice_line_values(moves, invoice_values, invoice) + # Devido ao KEY com sale_line_id aqui + # vem somente um registro + # Caso venha apenas uma linha porem sem + # sale_line_id é preciso ignora-la + if len(moves) != 1 or not moves.sale_line_id: + return values + + sale_line_id = moves.sale_line_id + values["sale_line_ids"] = [(6, 0, sale_line_id.ids)] + sale_line_id = moves.sale_line_id + analytic_account_id = sale_line_id.order_id.analytic_account_id.id + if sale_line_id.analytic_distribution and not sale_line_id.display_type: + values["analytic_distribution"] = sale_line_id.analytic_distribution + if analytic_account_id and not sale_line_id.display_type: + analytic_account_id = str(analytic_account_id) + if "analytic_distribution" in values: + values["analytic_distribution"][analytic_account_id] = ( + values["analytic_distribution"].get(analytic_account_id, 0) + 100 + ) + else: + values["analytic_distribution"] = {analytic_account_id: 100} + + return values