diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml new file mode 100644 index 0000000000..ef38974ae2 --- /dev/null +++ b/.github/workflows/initiate_release.yml @@ -0,0 +1,32 @@ +# This workflow is agnostic to branches. Only maintain on develop branch. +# To add/remove versions just modify the matrix. + +name: Create weekly release pull requests +on: + schedule: + # 9:30 UTC => 3 PM IST Tuesday + - cron: "30 9 * * 2" + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: ["13", "14"] + + steps: + - uses: octokit/request-action@v2.x + with: + route: POST /repos/{owner}/{repo}/pulls + owner: frappe + repo: erpnext + title: |- + "chore: release v${{ matrix.version }}" + body: "Automated weekly release." + base: version-${{ matrix.version }} + head: version-${{ matrix.version }}-hotfix + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/CODEOWNERS b/CODEOWNERS index b6aadb3f2e..e406f8f56e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,8 +14,8 @@ pos* @nextchamp-saqib erpnext/buying/ @rohitwaghchaure @s-aga-r erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r -erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r -erpnext/stock/ @marination @rohitwaghchaure @s-aga-r +erpnext/quality_management/ @rohitwaghchaure @s-aga-r +erpnext/stock/ @rohitwaghchaure @s-aga-r erpnext/crm/ @NagariaHussain erpnext/education/ @rutwikhdev diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1987c8340d..7227b95818 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -366,7 +366,7 @@ def update_outstanding_amt( if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: ref_doc = frappe.get_doc(against_voucher_type, against_voucher) - # Didn't use db_set for optimisation purpose + # Didn't use db_set for optimization purpose ref_doc.outstanding_amount = bal frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal) diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index 1c19c1d225..cf5fbe12af 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on("Journal Entry Template", { - setup: function(frm) { + refresh: function(frm) { frappe.model.set_default_values(frm.doc); frm.set_query("account" ,"accounts", function(){ diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 4596b00fc1..22842cec0f 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -22,7 +22,8 @@ "amount", "account_currency", "amount_in_account_currency", - "delinked" + "delinked", + "remarks" ], "fields": [ { @@ -136,12 +137,17 @@ "fieldtype": "Link", "label": "Finance Book", "options": "Finance Book" + }, + { + "fieldname": "remarks", + "fieldtype": "Text", + "label": "Remarks" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-07-11 09:13:54.379168", + "modified": "2022-08-22 15:32:56.629430", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index d5f7ee4f21..994b6776e3 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -43,6 +43,7 @@ "currency", "write_off_account", "write_off_cost_center", + "write_off_limit", "account_for_change_amount", "disable_rounded_total", "column_break_23", @@ -360,6 +361,14 @@ "fieldtype": "Check", "label": "Validate Stock on Save" }, + { + "default": "1", + "description": "Auto write off precision loss while consolidation", + "fieldname": "write_off_limit", + "fieldtype": "Currency", + "label": "Write Off Limit", + "reqd": 1 + }, { "default": "0", "description": "If enabled, the consolidated invoices will have rounded total disabled", @@ -393,7 +402,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2022-07-21 11:16:46.911173", + "modified": "2022-08-10 12:57:06.241439", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 8ec726b36c..1f88849b26 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_gl_entries(gl_entries=gl_entries, cancel=1) + make_gl_entries(gl_map=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 164ba6aa34..5a0aeb7284 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, "2019-01-10") + + def test_pda_submission_and_cancellation(self): + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-01-01", + start_date="2019-01-01", + end_date="2019-01-31", + type="Income", + ) + ) + pda.submit() + pda.cancel() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6ff1281d9..fea81e9c27 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -575,7 +575,6 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) - self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -807,7 +806,7 @@ class PurchaseInvoice(BuyingController): ) if not item.is_fixed_asset: - dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting) + dummy, amount = self.get_amount_and_base_amount(item, None) else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) @@ -1165,7 +1164,7 @@ class PurchaseInvoice(BuyingController): ) for tax in self.get("taxes"): - amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting) + amount, base_amount = self.get_tax_amounts(tax, None) if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e55d3a70af..0a4f25b876 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -338,59 +338,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) - @change_settings("Buying Settings", {"enable_discount_accounting": 1}) - def test_purchase_invoice_with_discount_accounting_enabled(self): - - discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - pi = make_purchase_invoice(discount_account=discount_account, rate=45) - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 225.0, nowdate()], - ["Discount Account - _TC", 0.0, 25.0, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - - @change_settings("Buying Settings", {"enable_discount_accounting": 1}) - def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - - additional_discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - - pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") - pi.apply_discount_on = "Grand Total" - pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 10 - pi.disable_rounded_total = 1 - pi.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10, - }, - ) - pi.submit() - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 247.5, nowdate()], - ["Discount Account - _TC", 0.0, 27.5, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index adcbb83e8d..73ec051c6d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -479,9 +479,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e is_cash_or_non_trade_discount() { this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount); + if (!this.frm.doc.is_cash_or_non_trade_discount) { this.frm.set_value("additional_discount_account", ""); } + + this.calculate_taxes_and_totals(); } }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 19a234d9df..1f5879d7bf 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -710,6 +710,7 @@ class SalesInvoice(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] @@ -1033,22 +1034,6 @@ class SalesInvoice(SellingController): ) ) - if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": self.debit_to, - "debit": self.base_discount_amount, - "debit_in_account_currency": self.discount_amount, - "cost_center": self.cost_center, - "project": self.project, - }, - self.currency, - item=self, - ) - ) - def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") @@ -2177,6 +2162,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_item(source, target, source_parent): target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item": + target.purchase_order = source.parent + target.purchase_order_item = source.name + + if ( + source.get("purchase_order") + and source.get("purchase_order_item") + and target.doctype == "Purchase Invoice Item" + ): + target.purchase_order = source.purchase_order + target.po_detail = source.purchase_order_item item_field_map = { "doctype": target_doctype + " Item", @@ -2203,6 +2199,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "serial_no": "serial_no", } ) + elif target_doctype == "Sales Order": + item_field_map["field_map"].update( + { + source_document_warehouse_field: "warehouse", + } + ) doclist = get_mapped_doc( doctype, @@ -2247,6 +2249,7 @@ def get_received_items(reference_name, doctype, reference_fieldname): def set_purchase_references(doc): # add internal PO or PR links if any + if doc.is_internal_transfer(): if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) @@ -2276,15 +2279,6 @@ def set_purchase_references(doc): warehouse_map, ) - if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = get_pd_details( - "Purchase Order Item", so_item_map, "sales_order_item" - ) - - update_pi_items( - doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map - ) - def update_pi_items( doc, @@ -2300,13 +2294,19 @@ def update_pi_items( item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): for item in doc.get("items"): - item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) - item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def get_delivery_note_details(internal_reference): diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index b417c7de03..4f97b63789 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -96,6 +96,10 @@ "delivery_note", "dn_detail", "delivered_qty", + "internal_transfer_section", + "purchase_order", + "column_break_92", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -282,7 +286,6 @@ "label": "Discount (%) on Price List Rate with Margin", "oldfieldname": "adj_rate", "oldfieldtype": "Float", - "precision": "2", "print_hide": 1 }, { @@ -841,12 +844,38 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_92", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:33:15.335912", + "modified": "2022-09-06 14:17:43.394309", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index a519d8be73..6004e2b19b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -318,7 +318,6 @@ def get_advance_vouchers( "is_cancelled": 0, "party_type": party_type, "party": ["in", parties], - "against_voucher": ["is", "not set"], } if company: diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 0238711a70..0b4e577f6c 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -178,6 +178,11 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Data", "hidden": 1 }, + { + "fieldname": "show_remarks", + "label": __("Show Remarks"), + "fieldtype": "Check", + }, { "fieldname": "customer_name", "label": __("Customer Name"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e937edbeb2..3f504b13f6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -119,6 +119,7 @@ class ReceivablePayableReport(object): party_account=ple.account, posting_date=ple.posting_date, account_currency=ple.account_currency, + remarks=ple.remarks, invoiced=0.0, paid=0.0, credit_note=0.0, @@ -178,6 +179,11 @@ class ReceivablePayableReport(object): key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) row = self.voucher_balance.get(key) + + if not row: + # no invoice, this is an invoice / stand-alone payment / credit note + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + return row def update_voucher_balance(self, ple): @@ -187,7 +193,11 @@ class ReceivablePayableReport(object): if not row: return - amount = ple.amount + # amount in "Party Currency", if its supplied. If not, amount in company currency + if self.filters.get(scrub(self.party_type)): + amount = ple.amount_in_account_currency + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher @@ -685,9 +695,10 @@ class ReceivablePayableReport(object): ple.party, ple.posting_date, ple.due_date, - ple.account_currency.as_("currency"), + ple.account_currency, ple.amount, ple.amount_in_account_currency, + ple.remarks, ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) @@ -722,6 +733,7 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] party_type_field = scrub(self.party_type) + self.qb_selection_filter.append(self.ple.party_type == self.party_type) self.add_common_filters(party_type_field=party_type_field) @@ -965,6 +977,9 @@ class ReceivablePayableReport(object): options="Supplier Group", ) + if self.filters.show_remarks: + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): if not fieldname: fieldname = scrub(label) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index edddbbce21..bac8beed2e 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,19 +1,27 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(unittest.TestCase): - def test_accounts_receivable(self): +class TestAccountsReceivable(FrappeTestCase): + def setUp(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") + frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + def tearDown(self): + frappe.db.rollback() + + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", "based_on_payment_terms": 1, @@ -66,6 +74,50 @@ class TestAccountsReceivable(unittest.TestCase): ], ) + def test_payment_againt_po_in_receivable_report(self): + """ + Payments made against Purchase Order will show up as outstanding amount + """ + + so = make_sales_order( + company="_Test Company 2", + customer="_Test Customer 2", + warehouse="Finished Goods - _TC2", + currency="EUR", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) + + pe = get_payment_entry(so.doctype, so.name) + pe = pe.save().submit() + + filters = { + "company": "_Test Company 2", + "based_on_payment_terms": 0, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report = execute(filters) + + expected_data_after_payment = [0, 1000, 0, -1000] + + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + def make_sales_invoice(): frappe.set_user("Administrator") diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 98dbbf6c44..330e442a80 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -535,7 +535,11 @@ def get_accounts(root_type, companies): ): if account.account_name not in added_accounts: accounts.append(account) - added_accounts.append(account.account_name) + if account.account_number: + account_key = account.account_number + "-" + account.account_name + else: + account_key = account.account_name + added_accounts.append(account_key) return accounts diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 21205c3163..615804ef62 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -4,7 +4,7 @@ frappe.query_reports["Gross Profit"] = { "filters": [ { - "fieldname":"company", + "fieldname": "company", "label": __("Company"), "fieldtype": "Link", "options": "Company", @@ -12,32 +12,44 @@ frappe.query_reports["Gross Profit"] = { "reqd": 1 }, { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), "reqd": 1 }, { - "fieldname":"sales_invoice", + "fieldname": "sales_invoice", "label": __("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice" }, { - "fieldname":"group_by", + "fieldname": "group_by", "label": __("Group By"), "fieldtype": "Select", "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", "default": "Invoice" }, + { + "fieldname": "item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "options": "Item Group" + }, + { + "fieldname": "sales_person", + "label": __("Sales Person"), + "fieldtype": "Link", + "options": "Sales Person" + }, ], "tree": true, "name_field": "parent", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 54af225915..f0106bea4f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -7,6 +7,7 @@ from frappe import _, scrub from frappe.utils import cint, flt, formatdate from erpnext.controllers.queries import get_match_cond +from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import get_incoming_rate @@ -676,6 +677,17 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" + if self.filters.item_group: + conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + + if self.filters.sales_person: + conditions += """ + and exists(select 1 + from `tabSales Team` st + where st.parent = `tabSales Invoice`.name + and st.sales_person = %(sales_person)s) + """ + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" @@ -723,6 +735,7 @@ class GrossProfitGenerator(object): from `tabSales Invoice` inner join `tabSales Invoice Item` on `tabSales Invoice Item`.parent = `tabSales Invoice`.name + join `tabItem` item on item.name = `tabSales Invoice Item`.item_code {sales_team_table} {payment_term_table} where diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 018e8f9301..f61e8ac960 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1424,6 +1424,7 @@ def create_payment_ledger_entry( "amount": dr_or_cr, "amount_in_account_currency": dr_or_cr_account_currency, "delinked": True if cancel else False, + "remarks": gle.remarks, } ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 986b7001ff..132840e38c 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1454,12 +1454,14 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass return item -def set_depreciation_settings_in_company(): - company = frappe.get_doc("Company", "_Test Company") - company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC" - company.depreciation_expense_account = "_Test Depreciations - _TC" - company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC" - company.depreciation_cost_center = "_Test Cost Center - _TC" +def set_depreciation_settings_in_company(company=None): + if not company: + company = "_Test Company" + company = frappe.get_doc("Company", company) + company.accumulated_depreciation_account = "_Test Accumulated Depreciations - " + company.abbr + company.depreciation_expense_account = "_Test Depreciations - " + company.abbr + company.disposal_account = "_Test Gain/Loss on Asset Disposal - " + company.abbr + company.depreciation_cost_center = "Main - " + company.abbr company.save() # Enable booking asset depreciation entry automatically diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index f5e4e723b4..f9ed2cc344 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -76,7 +76,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', { 'warehouse': frm.doc.warehouse, 'qty': item.consumed_quantity, 'serial_no': item.serial_no, - 'company': frm.doc.company + 'company': frm.doc.company, }; frappe.call({ diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index ba3189887c..accb5bf54b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -238,7 +238,6 @@ "no_copy": 1 }, { - "depends_on": "eval:!doc.__islocal", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -257,6 +256,7 @@ "fieldname": "stock_entry", "fieldtype": "Link", "label": "Stock Entry", + "no_copy": 1, "options": "Stock Entry", "read_only": 1 } @@ -264,10 +264,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-25 13:14:38.307723", + "modified": "2022-08-16 15:55:25.023471", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -303,6 +304,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "asset_name", "track_changes": 1, "track_seen": 1 diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5bf6011cf8..8758e9c17d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -1,11 +1,11 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import frappe from frappe import _ from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours +import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.controllers.accounts_controller import AccountsController @@ -17,7 +17,7 @@ class AssetRepair(AccountsController): self.update_status() if self.get("stock_items"): - self.set_total_value() + self.set_stock_items_cost() self.calculate_total_repair_cost() def update_status(self): @@ -26,7 +26,7 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() - def set_total_value(self): + def set_stock_items_cost(self): for item in self.get("stock_items"): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) @@ -66,6 +66,7 @@ class AssetRepair(AccountsController): if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=True) + self.db_set("stock_entry", None) if ( frappe.db.get_value("Asset", self.asset, "calculate_depreciation") and self.increase_in_asset_life @@ -133,6 +134,7 @@ class AssetRepair(AccountsController): "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate, "serial_no": stock_item.serial_no, + "cost_center": self.cost_center, }, ) @@ -142,72 +144,42 @@ class AssetRepair(AccountsController): self.db_set("stock_entry", stock_entry.name) def increase_stock_quantity(self): - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) - stock_entry.flags.ignore_links = True - stock_entry.cancel() + if self.stock_entry: + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) + stock_entry.flags.ignore_links = True + stock_entry.cancel() def make_gl_entries(self, cancel=False): - if flt(self.repair_cost) > 0: + if flt(self.total_repair_cost) > 0: gl_entries = self.get_gl_entries() make_gl_entries(gl_entries, cancel) def get_gl_entries(self): gl_entries = [] - repair_and_maintenance_account = frappe.db.get_value( - "Company", self.company, "repair_and_maintenance_account" - ) + fixed_asset_account = get_asset_account( "fixed_asset_account", asset=self.asset, company=self.company ) - expense_account = ( + self.get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account) + self.get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account) + + return gl_entries + + def get_gl_entries_for_repair_cost(self, gl_entries, fixed_asset_account): + if flt(self.repair_cost) <= 0: + return + + pi_expense_account = ( frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account ) - gl_entries.append( - self.get_gl_dict( - { - "account": expense_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company, - }, - item=self, - ) - ) - - if self.get("stock_consumption"): - # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) - for item in stock_entry.items: - gl_entries.append( - self.get_gl_dict( - { - "account": item.expense_account, - "credit": item.amount, - "credit_in_account_currency": item.amount, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company, - }, - item=self, - ) - ) - gl_entries.append( self.get_gl_dict( { "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": expense_account, + "debit": self.repair_cost, + "debit_in_account_currency": self.repair_cost, + "against": pi_expense_account, "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, @@ -220,7 +192,75 @@ class AssetRepair(AccountsController): ) ) - return gl_entries + gl_entries.append( + self.get_gl_dict( + { + "account": pi_expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": fixed_asset_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + + def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account): + if not (self.get("stock_consumption") and self.get("stock_items")): + return + + # creating GL Entries for each row in Stock Items based on the Stock Entry created for it + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) + + default_expense_account = None + if not erpnext.is_perpetual_inventory_enabled(self.company): + default_expense_account = frappe.get_cached_value( + "Company", self.company, "default_expense_account" + ) + if not default_expense_account: + frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company)) + + for item in stock_entry.items: + if flt(item.amount) > 0: + gl_entries.append( + self.get_gl_dict( + { + "account": item.expense_account or default_expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": fixed_asset_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + + gl_entries.append( + self.get_gl_dict( + { + "account": fixed_asset_account, + "debit": item.amount, + "debit_in_account_currency": item.amount, + "against": item.expense_account or default_expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Stock Entry", + "against_voucher": self.stock_entry, + "company": self.company, + }, + item=self, + ) + ) def modify_depreciation_schedule(self): for row in self.asset_doc.finance_books: diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 4e7cf78090..6e06f52ac6 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import flt, nowdate +from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.assets.doctype.asset.test_asset import ( create_asset, create_asset_data, @@ -125,10 +126,109 @@ class TestAssetRepair(unittest.TestCase): asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) self.assertTrue(asset_repair.purchase_invoice) - def test_gl_entries(self): - asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) - gl_entry = frappe.get_last_doc("GL Entry") - self.assertEqual(asset_repair.name, gl_entry.voucher_no) + def test_gl_entries_with_perpetual_inventory(self): + set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") + + asset_category = frappe.get_doc("Asset Category", "Computers") + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.save() + + asset_repair = create_asset_repair( + capitalize_repair_cost=1, + stock_consumption=1, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + submit=1, + ) + + gl_entries = frappe.db.sql( + """ + select + account, + sum(debit) as debit, + sum(credit) as credit + from `tabGL Entry` + where + voucher_type='Asset Repair' + and voucher_no=%s + group by + account + """, + asset_repair.name, + as_dict=1, + ) + + self.assertTrue(gl_entries) + + fixed_asset_account = get_asset_account( + "fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company + ) + pi_expense_account = ( + frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account + ) + stock_entry_expense_account = ( + frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account + ) + + expected_values = { + fixed_asset_account: [asset_repair.total_repair_cost, 0], + pi_expense_account: [0, asset_repair.repair_cost], + stock_entry_expense_account: [0, 100], + } + + for d in gl_entries: + self.assertEqual(expected_values[d.account][0], d.debit) + self.assertEqual(expected_values[d.account][1], d.credit) + + def test_gl_entries_with_periodical_inventory(self): + frappe.db.set_value( + "Company", "_Test Company", "default_expense_account", "Cost of Goods Sold - _TC" + ) + asset_repair = create_asset_repair( + capitalize_repair_cost=1, + stock_consumption=1, + submit=1, + ) + + gl_entries = frappe.db.sql( + """ + select + account, + sum(debit) as debit, + sum(credit) as credit + from `tabGL Entry` + where + voucher_type='Asset Repair' + and voucher_no=%s + group by + account + """, + asset_repair.name, + as_dict=1, + ) + + self.assertTrue(gl_entries) + + fixed_asset_account = get_asset_account( + "fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company + ) + default_expense_account = frappe.get_cached_value( + "Company", asset_repair.company, "default_expense_account" + ) + + expected_values = {fixed_asset_account: [1100, 0], default_expense_account: [0, 1100]} + + for d in gl_entries: + self.assertEqual(expected_values[d.account][0], d.debit) + self.assertEqual(expected_values[d.account][1], d.credit) def test_increase_in_asset_life(self): asset = create_asset(calculate_depreciation=1, submit=1) @@ -160,7 +260,7 @@ def create_asset_repair(**args): if args.asset: asset = args.asset else: - asset = create_asset(is_existing_asset=1, submit=1) + asset = create_asset(is_existing_asset=1, submit=1, company=args.company) asset_repair = frappe.new_doc("Asset Repair") asset_repair.update( { @@ -192,7 +292,7 @@ def create_asset_repair(**args): if args.submit: asset_repair.repair_status = "Completed" - asset_repair.cost_center = "_Test Cost Center - _TC" + asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center") if args.stock_consumption: stock_entry = frappe.get_doc( @@ -204,6 +304,8 @@ def create_asset_repair(**args): "t_warehouse": asset_repair.warehouse, "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity, + "basic_rate": args.rate if args.get("rate") is not None else 100, + "cost_center": asset_repair.cost_center, }, ) stock_entry.submit() @@ -213,7 +315,13 @@ def create_asset_repair(**args): asset_repair.repair_cost = 1000 if asset.calculate_depreciation: asset_repair.increase_in_asset_life = 12 - asset_repair.purchase_invoice = make_purchase_invoice().name + pi = make_purchase_invoice( + company=asset.company, + expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"), + cost_center=asset_repair.cost_center, + warehouse=asset_repair.warehouse, + ) + asset_repair.purchase_invoice = pi.name asset_repair.submit() return asset_repair diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 6c18a4650b..aad26075f2 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -76,7 +76,7 @@ "label": "Subcontracting Settings" }, { - "default": "Material Transferred for Subcontract", + "default": "BOM", "fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldtype": "Select", "label": "Backflush Raw Materials of Subcontract Based On", @@ -148,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:40:26.103909", + "modified": "2022-09-01 18:01:34.994657", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index aa50487d78..acca380672 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -60,6 +60,7 @@ "section_break_45", "before_items_section", "scan_barcode", + "set_from_warehouse", "items_col_break", "set_warehouse", "items_section", @@ -1166,13 +1167,20 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 + }, + { + "depends_on": "is_internal_supplier", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-07 11:06:46.035093", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 01b55c00d6..05b5a8e7b8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -23,5 +23,6 @@ def get_data(): "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, + {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index bd7e4e8d86..6c1bcc7dd4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -7,8 +7,10 @@ import json import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) @@ -796,6 +798,111 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def test_internal_transfer_flow(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + make_inter_company_purchase_invoice, + ) + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + frappe.db.set_value("Selling Settings", None, "maintain_same_sales_rate", 1) + frappe.db.set_value("Buying Settings", None, "maintain_same_rate", 1) + + prepare_data_for_internal_transfer() + supplier = "_Test Internal Supplier 2" + + po = create_purchase_order( + company="_Test Company with perpetual inventory", + supplier=supplier, + warehouse="Stores - TCP1", + from_warehouse="_Test Internal Warehouse New 1 - TCP1", + qty=2, + rate=1, + ) + + so = make_inter_company_sales_order(po.name) + so.items[0].delivery_date = today() + self.assertEqual(so.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(so.items[0].purchase_order) + self.assertTrue(so.items[0].purchase_order_item) + so.submit() + + dn = make_delivery_note(so.name) + dn.items[0].target_warehouse = "_Test Internal Warehouse GIT - TCP1" + self.assertEqual(dn.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(dn.items[0].purchase_order) + self.assertTrue(dn.items[0].purchase_order_item) + + self.assertEqual(po.items[0].name, dn.items[0].purchase_order_item) + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + self.assertEqual(pr.items[0].warehouse, "Stores - TCP1") + self.assertTrue(pr.items[0].purchase_order) + self.assertTrue(pr.items[0].purchase_order_item) + self.assertEqual(po.items[0].name, pr.items[0].purchase_order_item) + pr.submit() + + si = make_sales_invoice(so.name) + self.assertEqual(si.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(si.items[0].purchase_order) + self.assertTrue(si.items[0].purchase_order_item) + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + self.assertTrue(pi.items[0].purchase_order) + self.assertTrue(pi.items[0].po_detail) + pi.submit() + + po.load_from_db() + self.assertEqual(po.status, "Completed") + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company) + + create_warehouse("_Test Internal Warehouse GIT", company=company) + + make_purchase_receipt(company=company, warehouse=warehouse, qty=2, rate=100) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -847,6 +954,7 @@ def create_purchase_order(**args): { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", + "from_warehouse": args.from_warehouse, "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9845396f..82e92e87bc 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -10,12 +10,14 @@ "item_code", "supplier_part_no", "item_name", + "brand", "product_bundle", "fg_item", "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", + "item_group", "section_break_5", "description", "col_break1", @@ -58,9 +60,12 @@ "base_net_rate", "base_net_amount", "warehouse_and_reference", + "from_warehouse", "warehouse", + "column_break_54", "actual_qty", "company_total_stock", + "references_section", "material_request", "material_request_item", "sales_order", @@ -73,8 +78,6 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", - "item_group", - "brand", "section_break_56", "received_qty", "returned_qty", @@ -442,13 +445,13 @@ { "fieldname": "warehouse_and_reference", "fieldtype": "Section Break", - "label": "Warehouse and Reference" + "label": "Warehouse Settings" }, { "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "Warehouse", + "label": "Target Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -760,7 +763,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at Warehouse", + "label": "Available Qty at Target Warehouse", "print_hide": 1, "read_only": 1 }, @@ -868,13 +871,30 @@ "fieldtype": "Float", "label": "Finished Good Item Qty", "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" + }, + { + "depends_on": "eval:parent.is_internal_supplier", + "fieldname": "from_warehouse", + "fieldtype": "Link", + "label": "From Warehouse", + "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:29:40.602349", + "modified": "2022-09-07 11:12:38.634976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 70d2bc68a1..6f321f4766 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -373,7 +373,7 @@ class AccountsController(TransactionBase): ) def validate_inter_company_reference(self): - if self.doctype not in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): + if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): return if self.is_internal_transfer(): @@ -1109,17 +1109,17 @@ class AccountsController(TransactionBase): frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + if enable_discount_accounting: - if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" - rev_dr_cr = "debit" - supplier_or_customer = self.supplier - - else: - dr_or_cr = "debit" - rev_dr_cr = "credit" - supplier_or_customer = self.customer - for item in self.get("items"): if item.get("discount_amount") and item.get("discount_account"): discount_amount = item.discount_amount * item.qty @@ -1173,18 +1173,22 @@ class AccountsController(TransactionBase): ) ) - if self.get("discount_amount") and self.get("additional_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": supplier_or_customer, - dr_or_cr: self.discount_amount, - "cost_center": self.cost_center, - }, - item=self, - ) + if ( + (enable_discount_accounting or self.get("is_cash_or_non_trade_discount")) + and self.get("additional_discount_account") + and self.get("discount_amount") + ): + gl_entries.append( + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center, + }, + item=self, ) + ) def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index a3d41ab29a..5e9c069b1d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -311,6 +311,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": p.get("incoming_rate"), + "item_row": p, } ) ) @@ -334,6 +335,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": d.get("incoming_rate"), + "item_row": d, } ) ) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 197d2ba2dc..6e7d2b33c2 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -307,6 +307,20 @@ class StatusUpdater(Document): def limits_crossed_error(self, args, item, qty_or_amount): """Raise exception for limits crossed""" + if ( + self.doctype in ["Sales Invoice", "Delivery Note"] + and qty_or_amount == "amount" + and self.is_internal_customer + ): + return + + elif ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and qty_or_amount == "amount" + and self.is_internal_supplier + ): + return + if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2e526af658..9149b4d857 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -390,6 +390,10 @@ class StockController(AccountsController): return sl_dict def update_inventory_dimensions(self, row, sl_dict) -> None: + # To handle delivery note and sales invoice + if row.get("item_row"): + row = row.get("item_row") + dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) for dimension in dimensions: if not dimension: @@ -407,9 +411,17 @@ class StockController(AccountsController): "DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" ) + if not fieldname: + fieldname = frappe.get_cached_value( + "Custom Field", {"dt": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" + ) + if fieldname and self.get(fieldname): sl_dict[dimension.target_fieldname] = self.get(fieldname) + if sl_dict[dimension.target_fieldname] and self.docstatus == 1: + row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname]) + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 1372c89d47..21504774f6 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -7,6 +7,7 @@ from collections import defaultdict import frappe from frappe import _ +from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, cstr, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController @@ -870,7 +871,17 @@ def add_items_in_ste( def make_return_stock_entry_for_subcontract( available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" ): - ste_doc = frappe.new_doc("Stock Entry") + ste_doc = get_mapped_doc( + order_doctype, + order_doc.name, + { + order_doctype: { + "doctype": "Stock Entry", + }, + }, + ignore_child_tables=True, + ) + ste_doc.purpose = "Material Transfer" if order_doctype == "Purchase Order": diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 0d8cffe03f..cbcccce5f7 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,12 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + # Update grand total as per cash and non trade discount + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): + self.doc.grand_total -= self.doc.discount_amount + self.doc.base_grand_total -= self.doc.base_discount_amount + self.set_rounded_total() + self.calculate_shipping_charges() if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: @@ -500,9 +506,6 @@ class calculate_taxes_and_totals(object): else: self.doc.grand_total = flt(self.doc.net_total) - if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): - self.doc.grand_total -= self.doc.discount_amount - if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), @@ -597,16 +600,16 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) + self.doc.base_discount_amount = flt( + self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") + ) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get( "is_cash_or_non_trade_discount" ): self.discount_amount_applied = True return - self.doc.base_discount_amount = flt( - self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") - ) - total_for_discount_amount = self.get_total_for_discount_amount() taxes = self.doc.get("taxes") net_total = 0 @@ -767,6 +770,18 @@ class calculate_taxes_and_totals(object): self.doc.precision("outstanding_amount"), ) + if ( + self.doc.doctype == "Sales Invoice" + and self.doc.get("is_pos") + and self.doc.get("pos_profile") + and self.doc.get("is_consolidated") + ): + write_off_limit = flt( + frappe.db.get_value("POS Profile", self.doc.pos_profile, "write_off_limit") + ) + if write_off_limit and abs(self.doc.outstanding_amount) <= write_off_limit: + self.doc.write_off_outstanding_amount_automatically = 1 + if ( self.doc.doctype == "Sales Invoice" and self.doc.get("is_pos") diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5f5923dc89..6e7ba1fd5b 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -7,7 +7,7 @@ from collections import Counter import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_url, getdate +from frappe.utils import get_url, getdate, now from frappe.utils.verified_command import get_signed_params @@ -104,16 +104,28 @@ class Appointment(Document): # Return if already linked if self.party: return + lead = frappe.get_doc( { "doctype": "Lead", "lead_name": self.customer_name, "email_id": self.customer_email, - "notes": self.customer_details, "phone": self.customer_phone_number, } ) + + if self.customer_details: + lead.append( + "notes", + { + "note": self.customer_details, + "added_by": frappe.session.user, + "added_on": now(), + }, + ) + lead.insert(ignore_permissions=True) + # Link lead self.party = lead.name diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 776e604333..178b9d2de5 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -6,29 +6,20 @@ import unittest import frappe - -def create_test_lead(): - test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"}) - if test_lead: - return frappe.get_doc("Lead", test_lead) - test_lead = frappe.get_doc( - {"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"} - ) - test_lead.insert(ignore_permissions=True) - return test_lead +LEAD_EMAIL = "test_appointment_lead@example.com" -def create_test_appointments(): +def create_test_appointment(): test_appointment = frappe.get_doc( { "doctype": "Appointment", - "email": "test@example.com", "status": "Open", "customer_name": "Test Lead", "customer_phone_number": "666", "customer_skype": "test", - "customer_email": "test@example.com", + "customer_email": LEAD_EMAIL, "scheduled_time": datetime.datetime.now(), + "customer_details": "Hello, Friend!", } ) test_appointment.insert() @@ -36,16 +27,16 @@ def create_test_appointments(): class TestAppointment(unittest.TestCase): - test_appointment = test_lead = None + def setUpClass(): + frappe.db.delete("Lead", {"email_id": LEAD_EMAIL}) def setUp(self): - self.test_lead = create_test_lead() - self.test_appointment = create_test_appointments() + self.test_appointment = create_test_appointment() + self.test_appointment.set_verified(self.test_appointment.customer_email) def test_calendar_event_created(self): cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) def test_lead_linked(self): - lead = frappe.get_doc("Lead", self.test_lead.name) - self.assertIsNotNone(lead) + self.assertTrue(self.test_appointment.party) diff --git a/erpnext/e_commerce/product_ui/search.js b/erpnext/e_commerce/product_ui/search.js index 61922459e5..1688cc1fb6 100644 --- a/erpnext/e_commerce/product_ui/search.js +++ b/erpnext/e_commerce/product_ui/search.js @@ -200,7 +200,7 @@ erpnext.ProductSearch = class { let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png'; html += `