diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 9d99ebb7bb6..a5de50fb06f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.9.0' +__version__ = '14.0.0-dev' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 5df8269f754..22c81ddd463 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against, frappe.db.commit() except Exception as e: if frappe.flags.in_test: + traceback = frappe.get_traceback() + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) raise e else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) - + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True def send_mail(deferred_process): @@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, if submit: journal_entry.submit() + + frappe.db.commit() except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True diff --git a/erpnext/accounts/doctype/advance_tax/__init__.py b/erpnext/accounts/doctype/advance_tax/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.json b/erpnext/accounts/doctype/advance_tax/advance_tax.json new file mode 100644 index 00000000000..68706aba88f --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-25 10:24:39.836195", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "reference_detail", + "account_head", + "allocated_amount" + ], + "fields": [ + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "reference_detail", + "fieldtype": "Data", + "label": "Reference Detail" + }, + { + "fieldname": "account_head", + "fieldtype": "Link", + "label": "Account Head", + "options": "Account" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "label": "Allocated Amount", + "options": "party_account_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-11-25 10:27:51.712286", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Advance Tax", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.py b/erpnext/accounts/doctype/advance_tax/advance_tax.py new file mode 100644 index 00000000000..2e784efd8f3 --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AdvanceTax(Document): + pass diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json index 4d634994319..05b284ae16f 100644 --- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json +++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json @@ -25,8 +25,7 @@ "allocated_amount", "column_break_13", "base_tax_amount", - "base_total", - "base_allocated_amount" + "base_total" ], "fields": [ { @@ -168,12 +167,6 @@ "label": "Allocated Amount", "options": "currency" }, - { - "fieldname": "base_allocated_amount", - "fieldtype": "Currency", - "label": "Allocated Amount (Company Currency)", - "options": "Company:company:default_currency" - }, { "fetch_from": "account_head.account_currency", "fieldname": "currency", @@ -186,7 +179,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:46:58.373170", + "modified": "2021-11-25 11:10:10.945027", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Taxes and Charges", diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 5cbf00b2c69..e7371fbe436 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -434,7 +434,7 @@ def get_pi_matching_query(amount_condition): def get_ec_matching_query(bank_account, company, amount_condition): # get matching Expense Claim query - mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", + mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"])] mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' company_currency = get_company_currency(company) diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index bc8c6abeff3..69e13a407de 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -5,10 +5,10 @@ import unittest import frappe +from frappe.utils import now_datetime from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate -test_records = frappe.get_test_records('Fiscal Year') test_ignore = ["Company"] class TestFiscalYear(unittest.TestCase): @@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase): }) self.assertRaises(FiscalYearIncorrectDate, fy.insert) + + +def test_record_generator(): + test_records = [ + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2011", + "is_short_year": 1, + "year_end_date": "2011-04-01", + "year_start_date": "2011-12-31" + } + ] + + start = 2012 + end = now_datetime().year + 5 + for year in range(start, end): + test_records.append({ + "doctype": "Fiscal Year", + "year": f"_Test Fiscal Year {year}", + "year_start_date": f"{year}-01-01", + "year_end_date": f"{year}-12-31" + }) + + return test_records + +test_records = test_record_generator() diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json deleted file mode 100644 index 44052535cbd..00000000000 --- a/erpnext/accounts/doctype/fiscal_year/test_records.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "doctype": "Fiscal Year", - "year": "_Test Short Fiscal Year 2011", - "is_short_year": 1, - "year_end_date": "2011-04-01", - "year_start_date": "2011-12-31" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2012", - "year_end_date": "2012-12-31", - "year_start_date": "2012-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2013", - "year_end_date": "2013-12-31", - "year_start_date": "2013-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2014", - "year_end_date": "2014-12-31", - "year_start_date": "2014-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2015", - "year_end_date": "2015-12-31", - "year_start_date": "2015-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2016", - "year_end_date": "2016-12-31", - "year_start_date": "2016-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2017", - "year_end_date": "2017-12-31", - "year_start_date": "2017-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2018", - "year_end_date": "2018-12-31", - "year_start_date": "2018-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2019", - "year_end_date": "2019-12-31", - "year_start_date": "2019-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2020", - "year_end_date": "2020-12-31", - "year_start_date": "2020-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2021", - "year_end_date": "2021-12-31", - "year_start_date": "2021-01-01" - } -] diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index ee2e319a6fc..c8d1db91f54 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -61,7 +61,6 @@ "taxes_and_charges_section", "purchase_taxes_and_charges_template", "sales_taxes_and_charges_template", - "advance_tax_account", "column_break_55", "apply_tax_withholding_amount", "tax_withholding_category", @@ -685,15 +684,6 @@ "fieldtype": "Section Break", "hide_border": 1 }, - { - "depends_on": "eval:doc.apply_tax_withholding_amount", - "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices", - "fieldname": "advance_tax_account", - "fieldtype": "Link", - "label": "Advance Tax Account", - "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount", - "options": "Account" - }, { "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", @@ -730,7 +720,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-10-22 17:50:24.632806", + "modified": "2021-11-24 18:58:24.919764", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 26fd16a4fd5..c1b056b9c70 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.controllers.accounts_controller import ( @@ -339,7 +339,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) + .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount"))) + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -433,23 +433,12 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - if not self.advance_tax_account: - frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - net_total = self.paid_amount - for reference in self.get("references"): - net_total_for_tds = 0 - if reference.reference_doctype == 'Purchase Order': - net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) - - if net_total_for_tds: - net_total = net_total_for_tds - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, - 'doctype': 'Purchase Invoice', + 'doctype': 'Payment Entry', 'supplier': self.party, 'posting_date': self.posting_date, 'net_total': net_total @@ -461,7 +450,6 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -623,7 +611,7 @@ class PaymentEntry(AccountsController): if not total_negative_outstanding: frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") - .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"), + .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")), self.party_type), InvalidPaymentEntry) elif paid_amount - additional_charges > total_negative_outstanding: @@ -689,6 +677,7 @@ class PaymentEntry(AccountsController): self.add_deductions_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries) + gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) def add_party_gl_entries(self, gl_entries): @@ -752,7 +741,8 @@ class PaymentEntry(AccountsController): "against": self.party if self.payment_type=="Pay" else self.paid_to, "credit_in_account_currency": self.paid_amount, "credit": self.base_paid_amount, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "post_net_value": True }, item=self) ) if self.payment_type in ("Receive", "Internal Transfer"): @@ -782,14 +772,10 @@ class PaymentEntry(AccountsController): rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_to - payment_or_advance_account = self.get_party_account_for_taxes() + payment_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount base_tax_amount = d.base_tax_amount - if self.advance_tax_account: - tax_amount = -1 * tax_amount - base_tax_amount = -1 * base_tax_amount - gl_entries.append( self.get_gl_dict({ "account": d.account_head, @@ -798,19 +784,21 @@ class PaymentEntry(AccountsController): dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, - "cost_center": d.cost_center + "cost_center": d.cost_center, + "post_net_value": True, }, account_currency, item=d)) - if not d.included_in_paid_amount or self.advance_tax_account: + if not d.included_in_paid_amount: gl_entries.append( self.get_gl_dict({ - "account": payment_or_advance_account, + "account": payment_account, "against": against, rev_dr_or_cr: tax_amount, rev_dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "post_net_value": True, }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -832,9 +820,7 @@ class PaymentEntry(AccountsController): ) def get_party_account_for_taxes(self): - if self.advance_tax_account: - return self.advance_tax_account - elif self.payment_type == 'Receive': + if self.payment_type == 'Receive': return self.paid_to elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_from @@ -1106,7 +1092,7 @@ def get_outstanding_reference_documents(args): if not data: frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.") - .format(args.get("party_type").lower(), frappe.bold(args.get("party")))) + .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))) return data @@ -1599,13 +1585,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= }) pe.set_difference_amount() - if doc.doctype == 'Purchase Order' and doc.apply_tds: - pe.apply_tax_withholding_amount = 1 - pe.tax_withholding_category = doc.tax_withholding_category - - if not pe.advance_tax_account: - pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account') - return pe def get_bank_cash_account(doc, bank_account): diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index bff85872781..0c6e7edeb02 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -171,6 +171,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -1561,16 +1562,23 @@ "label": "Coupon Code", "options": "Coupon Code", "print_hide": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-27 20:12:57.306772", + "modified": "2021-10-05 12:11:53.871828", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 8b71eb02fd7..3f85668eded 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -46,6 +46,7 @@ "base_amount", "pricing_rules", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -800,14 +801,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-04 17:34:49.924531", + "modified": "2021-10-05 12:23:47.506290", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index efdeb1a5e81..813d20dbf93 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -3,22 +3,20 @@ {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.ui.form.on("POS Profile", "onload", function(frm) { - frm.set_query("selling_price_list", function() { - return { filters: { selling: 1 } }; - }); - - frm.set_query("tc_name", function() { - return { filters: { selling: 1 } }; - }); - - erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); - }); -}); - frappe.ui.form.on('POS Profile', { setup: function(frm) { + frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + + frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); + frm.set_query("print_format", function() { return { filters: [ @@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("account_for_change_amount", function() { + frm.set_query("account_for_change_amount", function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + is_group: 0, + company: doc.company } }; }); @@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', { }); frm.set_query('company_address', function(doc) { - if(!doc.company) { + if (!doc.company) { frappe.throw(__('Please set Company')); } @@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query('income_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'is_group': 0, + 'company': doc.company, + 'account_type': "Income Account" + } + }; + }); + + frm.set_query('cost_center', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'company': doc.company, + 'is_group': 0 + } + }; + }); + + frm.set_query('expense_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + "report_type": "Profit and Loss", + "company": doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("select_print_heading", function() { + return { + filters: [ + ['Print Heading', 'docstatus', '!=', 2] + ] + }; + }); + + frm.set_query("write_off_account", function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'is_group': 0, + 'company': doc.company + } + }; + }); + + frm.set_query("write_off_cost_center", function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { - if(frm.doc.company) { + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } }, @@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', { frm.toggle_display('expense_account', erpnext.is_perpetual_inventory_enabled(frm.doc.company)); } -}) - -// Income Account -// -------------------------------- -cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company, - 'account_type': "Income Account" - } - }; -}; - - -// Cost Center -// ----------------------------- -cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'company': doc.company, - 'is_group': 0 - } - }; -}; - - -// Expense Account -// ----------------------------- -cur_frm.fields_dict["expense_account"].get_query = function(doc) { - return { - filters: { - "report_type": "Profit and Loss", - "company": doc.company, - "is_group": 0 - } - }; -}; - -// ------------------ Get Print Heading ------------------------------------ -cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) { - return{ - filters:[ - ['Print Heading', 'docstatus', '!=', 2] - ] - }; -}; - -cur_frm.fields_dict.write_off_account.get_query = function(doc) { - return{ - filters:{ - 'report_type': 'Profit and Loss', - 'is_group': 0, - 'company': doc.company - } - }; -}; - -// Write off cost center -// ----------------------- -cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company - } - }; -}; +}); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 03cbc4acbc4..bd0116443ff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -130,6 +130,7 @@ "allocate_advances_automatically", "get_advances", "advances", + "advance_tax", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", @@ -1408,13 +1409,21 @@ { "fieldname": "column_break_147", "fieldtype": "Column Break" + }, + { + "fieldname": "advance_tax", + "fieldtype": "Table", + "hidden": 1, + "label": "Advance Tax", + "options": "Advance Tax", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-10-12 20:55:16.145651", + "modified": "2021-11-25 13:31:02.716727", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 62e3dc83468..48b5cb9459c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -427,6 +427,7 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.update_advance_tax_references() self.process_common_party_accounting() @@ -472,8 +473,6 @@ class PurchaseInvoice(BuyingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - gl_entries = make_regional_gl_entries(gl_entries, self) gl_entries = merge_similar_entries(gl_entries) @@ -729,7 +728,7 @@ class PurchaseInvoice(BuyingController): "account": self.stock_received_but_not_billed, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), - "remarks": self.remarks or "Accounting Entry for Stock", + "remarks": self.remarks or _("Accounting Entry for Stock"), "cost_center": self.cost_center, "project": item.project or self.project }, item=item) @@ -937,7 +936,7 @@ class PurchaseInvoice(BuyingController): "cost_center": tax.cost_center, "against": self.supplier, "credit": valuation_tax[tax.name], - "remarks": self.remarks or "Accounting Entry for Stock" + "remarks": self.remarks or _("Accounting Entry for Stock") }, item=tax)) @property @@ -1074,6 +1073,7 @@ class PurchaseInvoice(BuyingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.update_advance_tax_references(cancel=1) def update_project(self): project_list = [] @@ -1150,7 +1150,10 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) + tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category) + + # Adjust TDS paid on advances + self.allocate_advance_tds(tax_withholding_details, advance_taxes) if not tax_withholding_details: return @@ -1174,6 +1177,39 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() + def allocate_advance_tds(self, tax_withholding_details, advance_taxes): + self.set('advance_tax', []) + for tax in advance_taxes: + allocated_amount = 0 + pending_amount = flt(tax.tax_amount - tax.allocated_amount) + if flt(tax_withholding_details.get('tax_amount')) >= pending_amount: + tax_withholding_details['tax_amount'] -= pending_amount + allocated_amount = pending_amount + elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount: + allocated_amount = tax_withholding_details['tax_amount'] + tax_withholding_details['tax_amount'] = 0 + + self.append('advance_tax', { + 'reference_type': 'Payment Entry', + 'reference_name': tax.parent, + 'reference_detail': tax.name, + 'account_head': tax.account_head, + 'allocated_amount': allocated_amount + }) + + def update_advance_tax_references(self, cancel=0): + for tax in self.get('advance_tax'): + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + if cancel: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount - tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + else: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount + tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get('amended_from'): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 78396a5e589..aa2408e092e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1160,25 +1160,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Order with TDS applied po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', posting_date='2021-09-15') - po.apply_tds = 1 - po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() po.submit() - # Update Unrealized Profit / Loss Account which is used as default advance tax account - frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC') - # Create Payment Entry Against the order payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) payment_entry.paid_from = 'Cash - _TC' + payment_entry.apply_tax_withholding_amount = 1 + payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual' payment_entry.save() payment_entry.submit() # Check GLE for Payment Entry expected_gle = [ - ['_Test Account Excise Duty - _TC', 3000, 0], ['Cash - _TC', 0, 27000], - ['Creditors - _TC', 27000, 0], + ['Creditors - _TC', 30000, 0], ['TDS Payable - _TC', 0, 3000], ] @@ -1204,9 +1200,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Zero net effect on final TDS Payable on invoice expected_gle = [ ['_Test Account Cost for Goods Sold - _TC', 30000], - ['_Test Account Excise Duty - _TC', -3000], - ['Creditors - _TC', -27000], - ['TDS Payable - _TC', 0] + ['Creditors - _TC', -30000] ] gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount @@ -1219,6 +1213,14 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.amount) + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000) + + purchase_invoice.cancel() + + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date from `tabGL Entry` diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a1f3ee4b063..39dfd8d76d5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { } } -// project name -//-------------------------- -cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) { - return{ - query: "erpnext.controllers.queries.get_project_name", - filters: {'customer': doc.customer} - } -} - // Income Account in Details Table // -------------------------------- cur_frm.set_query("income_account", "items", function(doc) { @@ -978,7 +969,7 @@ frappe.ui.form.on('Sales Invoice', { } if (frm.doc.is_debit_note) { - frm.set_df_property('return_against', 'label', 'Adjustment Against'); + frm.set_df_property('return_against', 'label', __('Adjustment Against')); } if (frappe.boot.active_domains.includes("Healthcare")) { @@ -988,10 +979,10 @@ frappe.ui.form.on('Sales Invoice', { if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) { frm.add_custom_button(__('Healthcare Services'), function() { get_healthcare_services_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); frm.add_custom_button(__('Prescriptions'), function() { get_drugs_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); } } else { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 93e32f1a18c..545abf77e6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -182,6 +182,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -2019,6 +2020,12 @@ "label": "Total Billing Hours", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2031,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-11 20:19:38.667508", + "modified": "2021-10-21 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2086,4 +2093,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 59d46fc2e87..c4d59f1ef71 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -842,8 +842,6 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - self.make_item_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b5453ac56eb..6a488ea96ef 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2385,6 +2385,29 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_sales_commission(self): + si = frappe.copy_doc(test_records[0]) + item = copy.deepcopy(si.get('items')[0]) + item.update({ + "qty": 1, + "rate": 500, + "grant_commission": 1 + }) + si.append("items", item) + + # Test valid values + for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): + si.commission_rate = commission_rate + si.save() + self.assertEqual(si.amount_eligible_for_commission, 500) + self.assertEqual(si.total_commission, total_commission) + + # Test invalid values + for commission_rate in (101, -1): + si.reload() + si.commission_rate = commission_rate + self.assertRaises(frappe.ValidationError, si.save) + def test_sales_invoice_submission_post_account_freezing_date(self): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) @@ -2397,6 +2420,32 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + def test_over_billing_case_against_delivery_note(self): + ''' + Test a case where duplicating the item with qty = 1 in the invoice + allows overbilling even if it is disabled + ''' + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance') + frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0) + + dn = create_delivery_note() + dn.submit() + + si = make_sales_invoice(dn.name) + # make a copy of first item and add it to invoice + item_copy = frappe.copy_doc(si.items[0]) + si.append('items', item_copy) + si.save() + + with self.assertRaises(frappe.ValidationError) as err: + si.submit() + + self.assertTrue("cannot overbill" in str(err.exception).lower()) + + frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' 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 b90f3f09045..ae9ac35729a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -47,6 +47,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -828,15 +829,23 @@ "fieldtype": "Link", "label": "Discount Account", "options": "Account" + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-19 13:41:53.435827", + "modified": "2021-10-05 12:24:54.968907", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", 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 dc1818a052a..5bb9b931854 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -95,7 +95,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') .format(tax_withholding_category, inv.company, party)) - tax_amount, tax_deducted = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no @@ -106,7 +106,10 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) - return tax_row + if inv.doctype == 'Purchase Invoice': + return tax_row, tax_deducted_on_advances + else: + return tax_row def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -194,6 +197,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers + tax_deducted_on_advances = 0 + + if inv.doctype == 'Purchase Invoice': + tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 if taxable_vouchers: @@ -223,7 +230,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted + return tax_amount, tax_deducted, tax_deducted_on_advances def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' @@ -281,6 +288,29 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] +def get_taxes_deducted_on_advances_allocated(inv, tax_details): + advances = [d.reference_name for d in inv.get('advances')] + tax_info = [] + + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = frappe.qb.from_(at).inner_join(pe).on( + pe.name == at.parent + ).select( + at.parent, at.name, at.tax_amount, at.allocated_amount + ).where( + pe.tax_withholding_category == tax_details.get('tax_withholding_category') + ).where( + at.parent.isin(advances) + ).where( + at.account_head == tax_details.account_head + ).run(as_dict=True) + + return tax_info + + def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 8ef7d7e103c..1836db6477f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -73,8 +73,28 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency) entry.credit_in_account_currency = 0.0 + update_net_values(entry) + return gl_map +def update_net_values(entry): + # In some scenarios net value needs to be shown in the ledger + # This method updates net values as debit or credit + if entry.post_net_value and entry.debit and entry.credit: + if entry.debit > entry.credit: + entry.debit = entry.debit - entry.credit + entry.debit_in_account_currency = entry.debit_in_account_currency \ + - entry.credit_in_account_currency + entry.credit = 0 + entry.credit_in_account_currency = 0 + else: + entry.credit = entry.credit - entry.debit + entry.credit_in_account_currency = entry.credit_in_account_currency \ + - entry.debit_in_account_currency + + entry.debit = 0 + entry.debit_in_account_currency = 0 + def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e904768b386..6b4b43d30b7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - if not party_details.get("taxes_and_charges"): - party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) + + if tax_template: + party_details['taxes_and_charges'] = tax_template if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88bcdad7102..353f9087f1b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -109,7 +109,11 @@ class ReceivablePayableReport(object): invoiced = 0.0, paid = 0.0, credit_note = 0.0, - outstanding = 0.0 + outstanding = 0.0, + invoiced_in_account_currency = 0.0, + paid_in_account_currency = 0.0, + credit_note_in_account_currency = 0.0, + outstanding_in_account_currency = 0.0 ) self.get_invoices(gle) @@ -150,21 +154,28 @@ class ReceivablePayableReport(object): # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) + gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) + if gle_balance > 0: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency else: # invoice row.invoiced += gle_balance + row.invoiced_in_account_currency += gle_balance_in_account_currency else: # payment or credit note for receivables if self.is_invoice(gle): # stand alone debit / credit note row.credit_note -= gle_balance + row.credit_note_in_account_currency -= gle_balance_in_account_currency else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency + if gle.cost_center: row.cost_center = str(gle.cost_center) @@ -216,8 +227,13 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ + row.credit_note_in_account_currency, self.currency_precision) + row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + + if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ + (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -583,12 +599,14 @@ class ReceivablePayableReport(object): else: select_fields = "debit, credit" + doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" + remarks = ", remarks" if self.filters.get("show_remarks") else "" self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0} {remarks} + against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} from `tabGL Entry` where @@ -596,8 +614,8 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""" + .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -718,6 +736,13 @@ class ReceivablePayableReport(object): # get the balance of the GL (debit - credit) or reverse balance based on report type return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) + def get_gle_balance_in_account_currency(self, gle): + # get the balance of the GL (debit - credit) or reverse balance based on report type + return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + + def get_reverse_balance_in_account_currency(self, gle): + return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index e24a5f99184..d3e836afd10 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Include Default Book Entries"), "fieldtype": "Check", "default": 1 + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" } ], "formatter": function(value, row, column, data, default_formatter) { 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 c71bc17ca79..01799d58041 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import ( get_cash_flow_accounts, ) from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary -from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts +from erpnext.accounts.report.financial_statements import ( + filter_out_zero_value_rows, + get_fiscal_year_data, + sort_accounts, +) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_chart_data as get_pl_chart_data, ) @@ -265,7 +269,7 @@ def get_columns(companies, filters): return columns def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name = get_account_heads(root_type, + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) if not accounts: return [] @@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) @@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - return accounts, accounts_by_name + return accounts, accounts_by_name, parent_children_map def update_parent_account_names(accounts): """Update parent_account_name in accounts list. diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json index a740de35729..9dd4e437f7f 100644 --- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json +++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json @@ -1,27 +1,30 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-06 12:28:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-03-06 05:52:57.645281", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Partners Commission", - "owner": "Administrator", - "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", - "ref_doctype": "Sales Invoice", - "report_name": "Sales Partners Commission", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-05-06 12:28:23", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "modified": "2021-10-06 06:26:07.881340", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partners Commission", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", + "ref_doctype": "Sales Invoice", + "report_name": "Sales Partners Commission", + "report_type": "Query Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } ] -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d5..c2b1bbcf142 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button("Transfer Asset", function() { + frm.add_custom_button(__("Transfer Asset"), function() { erpnext.asset.transfer_asset(frm); }, __("Manage")); - frm.add_custom_button("Scrap Asset", function() { + frm.add_custom_button(__("Scrap Asset"), function() { erpnext.asset.scrap_asset(frm); }, __("Manage")); - frm.add_custom_button("Sell Asset", function() { + frm.add_custom_button(__("Sell Asset"), function() { frm.trigger("make_sales_invoice"); }, __("Manage")); } else if (frm.doc.status=='Scrapped') { - frm.add_custom_button("Restore Asset", function() { + frm.add_custom_button(__("Restore Asset"), function() { erpnext.asset.restore_asset(frm); }, __("Manage")); } @@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', { } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("View General Ledger", function() { + frm.add_custom_button(__("View General Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.available_for_use_date, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 351556c76c1..6a476f6903c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -192,8 +192,7 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if self.docstatus == 1 and finance_book.value_after_depreciation: - value_after_depreciation = (flt(finance_book.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(finance_book.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -242,7 +241,7 @@ class Asset(AccountsController): break # For first row - if has_pro_rata and n==0: + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, self.available_for_use_date, finance_book.depreciation_start_date) @@ -255,7 +254,7 @@ class Asset(AccountsController): if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(finance_book.frequency_of_depreciation)) + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount @@ -380,7 +379,12 @@ class Asset(AccountsController): # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = self.get_modified_available_for_use_date(row) + days = date_diff(row.depreciation_start_date, from_date) + 1 # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) @@ -390,6 +394,9 @@ class Asset(AccountsController): return has_pro_rata + def get_modified_available_for_use_date(self, row): + return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") @@ -428,10 +435,11 @@ class Asset(AccountsController): # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: - depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: + depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row return depreciation_amount_for_last_row @@ -876,13 +884,11 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + depreciation_amount = (flt(asset.gross_purchase_amount) - + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ca10b1db19a..874fb630f87 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_entry = { - "account": accumulated_depreciation_account, + "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None): } debit_entry = { - "account": depreciation_expense_account, + "account": debit_account, "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -132,6 +134,20 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account +def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): + root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") + + if root_type == "Expense": + credit_account = accumulated_depreciation_account + debit_account = depreciation_expense_account + elif root_type == "Income": + credit_account = depreciation_expense_account + debit_account = accumulated_depreciation_account + else: + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) + + return credit_account, debit_account + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a297a5a5013..44c4ce542d0 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup): calculate_depreciation = 1, available_for_use_date = "2030-06-06", is_existing_asset = 1, - number_of_depreciations_booked = 1, - opening_accumulated_depreciation = 40000, + number_of_depreciations_booked = 2, + opening_accumulated_depreciation = 47095.89, expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", + depreciation_start_date = "2032-12-31", total_number_of_depreciations = 3, frequency_of_depreciation = 12 ) self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2030-12-31", 14246.58, 54246.58], - ["2031-12-31", 25000.00, 79246.58], - ["2032-06-06", 10753.42, 90000.00] + ["2032-12-31", 30000.0, 77095.89], + ["2033-06-06", 12904.11, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] @@ -869,6 +868,72 @@ class TestDepreciationBasics(AssetSetup): self.assertFalse(asset.schedules[1].journal_entry) self.assertFalse(asset.schedules[2].journal_entry) + def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): + """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + else: + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + + def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): + """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account.""" + + depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") + depr_expense_account.root_type = "Income" + depr_expense_account.parent_account = "Income - _TC" + depr_expense_account.save() + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + else: + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + + # resetting + depr_expense_account.root_type = "Expense" + depr_expense_account.parent_account = "Expenses - _TC" + depr_expense_account.save() + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 51ce157a81c..c702687072d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { - "root_type": "Expense", + "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name } diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e2f3ca318f6..bd573bf479d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -42,10 +42,10 @@ class AssetCategory(Document): def validate_account_types(self): account_type_map = { - 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, - 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'root_type': 'Expense' }, - 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + 'fixed_asset_account': {'account_type': ['Fixed Asset']}, + 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, + 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, + 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} } for d in self.accounts: for fieldname in account_type_map.keys(): @@ -53,11 +53,11 @@ class AssetCategory(Document): selected_account = d.get(fieldname) key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) - expected_key_type = account_type_map[fieldname][key_to_match] + expected_key_types = account_type_map[fieldname][key_to_match] - if selected_key_type != expected_key_type: + if selected_key_type not in expected_key_types: frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), title=_("Invalid Account")) def valide_cwip_account(self): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 18a56d33e6d..d554d52a718 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', { if (frm.doc.repair_status == "Completed") { frm.set_value('completion_date', frappe.datetime.now_datetime()); } + }, + + stock_items_on_form_rendered() { + erpnext.setup_serial_or_batch_no(); } }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d780c18ad02..36848e9f15c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -118,9 +118,10 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, - "item_code": stock_item.item, + "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, - "basic_rate": stock_item.valuation_rate + "basic_rate": stock_item.valuation_rate, + "serial_no": stock_item.serial_no }) stock_entry.insert() diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 81b4f6c4499..7c0d05748e1 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -11,12 +11,15 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): set_depreciation_settings_in_company() create_asset_data() + create_item("_Test Stock Item") frappe.db.sql("delete from `tabTax Rule`") def test_update_status(self): @@ -70,9 +73,28 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) - self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + def test_serialized_item_consumption(self): + from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + stock_entry = make_serialized_item() + serial_nos = stock_entry.get("items")[0].serial_no + serial_no = serial_nos.split("\n")[0] + + # should not raise any error + create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code, + warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1) + + # should raise error + asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC", + item_code = stock_entry.get("items")[0].item_code) + + asset_repair.repair_status = "Completed" + self.assertRaises(SerialNoRequiredError, asset_repair.submit) + def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation = 1, submit=1) initial_asset_value = get_asset_value(asset) @@ -137,11 +159,12 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item": args.item or args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, - "consumed_quantity": args.qty or 1 + "consumed_quantity": args.qty or 1, + "serial_no": args.serial_no }) asset_repair.insert(ignore_if_duplicate=True) @@ -158,7 +181,7 @@ def create_asset_repair(**args): }) stock_entry.append('items', { "t_warehouse": asset_repair.warehouse, - "item_code": asset_repair.stock_items[0].item, + "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity }) stock_entry.submit() diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 528f0ec986a..f63add12356 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -5,19 +5,13 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "item", + "item_code", "valuation_rate", "consumed_quantity", - "total_value" + "total_value", + "serial_no" ], "fields": [ - { - "fieldname": "item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item", - "options": "Item" - }, { "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", @@ -38,12 +32,24 @@ "in_list_view": 1, "label": "Total Value", "read_only": 1 + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-12 03:19:55.006300", + "modified": "2021-11-11 18:23:00.492483", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 32659d607b0..14d2ccdb50d 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -11,7 +11,11 @@ from frappe.contacts.address_and_contact import ( ) from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( # noqa + get_dashboard_info, + get_timeline_data, + validate_party_accounts, +) from erpnext.utilities.transaction_base import TransactionBase diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3190feac705..2c92820a74d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -145,11 +145,6 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() - if self.doctype == 'Purchase Invoice': - self.calculate_paid_amount() - # apply tax withholding only if checked and applicable - self.set_tax_withholding() - if self.doctype in ['Purchase Invoice', 'Sales Invoice']: pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): @@ -164,6 +159,11 @@ class AccountsController(TransactionBase): self.set_inter_company_account() + if self.doctype == 'Purchase Invoice': + self.calculate_paid_amount() + # apply tax withholding only if checked and applicable + self.set_tax_withholding() + validate_regional(self) if self.doctype != 'Material Request': @@ -250,7 +250,12 @@ class AccountsController(TransactionBase): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) - if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: + if self.doctype in ( + 'Sales Order', + 'Delivery Note', + 'Sales Invoice', + 'POS Invoice', + ): self.calculate_commission() self.calculate_contribution() @@ -524,7 +529,8 @@ class AccountsController(TransactionBase): 'is_opening': self.get("is_opening") or "No", 'party_type': None, 'party': None, - 'project': self.get("project") + 'project': self.get("project"), + 'post_net_value': args.get('post_net_value') }) accounting_dimensions = get_accounting_dimensions() @@ -805,7 +811,6 @@ class AccountsController(TransactionBase): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - self.update_allocated_advance_taxes_on_cancel() if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): unlink_ref_doc_from_payment_entries(self) @@ -853,29 +858,6 @@ class AccountsController(TransactionBase): return tax_map - def update_allocated_advance_taxes_on_cancel(self): - if self.get('advances'): - tax_accounts = [d.account_head for d in self.get('taxes')] - allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'], - filters={'voucher_no': self.name, 'account': ('in', tax_accounts)}, - group_by='account', as_list=1)) - - tax_map = self.get_tax_map() - - for pe in self.get('advances'): - if pe.reference_type == 'Payment Entry': - pe = frappe.get_doc('Payment Entry', pe.reference_name) - for tax in pe.get('taxes'): - allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head) - if allocated_amount > tax.tax_amount: - allocated_amount = tax.tax_amount - - if allocated_amount: - frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount', - tax.allocated_amount - allocated_amount) - tax_map[tax.account_head] -= allocated_amount - allocated_tax_map[tax.account_head] -= allocated_amount - def get_amount_and_base_amount(self, item, enable_discount_accounting): amount = item.net_amount base_amount = item.base_net_amount @@ -959,58 +941,10 @@ class AccountsController(TransactionBase): }, item=self) ) - def allocate_advance_taxes(self, gl_entries): - tax_map = self.get_tax_map() - for pe in self.get("advances"): - if pe.reference_type == "Payment Entry" and \ - frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'): - pe = frappe.get_doc("Payment Entry", pe.reference_name) - for tax in pe.get("taxes"): - account_currency = get_account_currency(tax.account_head) - - if self.doctype == "Purchase Invoice": - dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - else: - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - - party = self.supplier if self.doctype == "Purchase Invoice" else self.customer - unallocated_amount = tax.tax_amount - tax.allocated_amount - if tax_map.get(tax.account_head): - amount = tax_map.get(tax.account_head) - if amount < unallocated_amount: - unallocated_amount = amount - - gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": party, - dr_or_cr: unallocated_amount, - dr_or_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": tax.cost_center - }, account_currency, item=tax)) - - gl_entries.append( - self.get_gl_dict({ - "account": pe.advance_tax_account, - "against": party, - rev_dr_cr: unallocated_amount, - rev_dr_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": tax.cost_center - }, account_currency, item=tax)) - - frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount", - tax.allocated_amount + unallocated_amount) - - tax_map[tax.account_head] -= unallocated_amount def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for + item_allowance = {} global_qty_allowance, global_amount_allowance = None, None @@ -1031,12 +965,7 @@ class AccountsController(TransactionBase): .format(item.item_code, ref_dt), title=_("Warning"), indicator="orange") continue - already_billed = frappe.db.sql(""" - select sum(%s) - from `tab%s` - where %s=%s and docstatus=1 and parent != %s - """ % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'), - (item.get(item_ref_dn), self.name))[0][0] + already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on) total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item)) @@ -1064,6 +993,43 @@ class AccountsController(TransactionBase): frappe.msgprint(_("Overbilling of {} ignored because you have {} role.") .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True) + def get_billed_amount_for_item(self, item, item_ref_dn, based_on): + ''' + Returns Sum of Amount of + Sales/Purchase Invoice Items + that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`) + that are submitted OR not submitted but are under current invoice + ''' + + from frappe.query_builder import Criterion + from frappe.query_builder.functions import Sum + + item_doctype = frappe.qb.DocType(item.doctype) + based_on_field = frappe.qb.Field(based_on) + join_field = frappe.qb.Field(item_ref_dn) + + result = ( + frappe.qb.from_(item_doctype) + .select(Sum(based_on_field)) + .where( + join_field == item.get(item_ref_dn) + ).where( + Criterion.any([ # select all items from other invoices OR current invoices + Criterion.all([ # for selecting items from other invoices + item_doctype.docstatus == 1, + item_doctype.parent != self.name + ]), + Criterion.all([ # for selecting items from current invoice, that are linked to same reference + item_doctype.docstatus == 0, + item_doctype.parent == self.name, + item_doctype.name != item.name + ]) + ]) + ) + ).run() + + return result[0][0] if result else 0 + def throw_overbill_exception(self, item, max_allowed_amt): frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") .format(item.item_code, item.idx, max_allowed_amt)) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 76a7d1a95ff..dc04dab84c3 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -539,6 +539,10 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) dimension_filters = get_dimension_filter_map() dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) query_filters = [] + or_filters = [] + fields = ['name'] + + searchfields = frappe.get_meta(doctype).get_search_fields() meta = frappe.get_meta(doctype) if meta.is_tree: @@ -550,8 +554,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) if meta.has_field('company'): query_filters.append(['company', '=', filters.get('company')]) - if txt: - query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt]) + for field in searchfields: + or_filters.append([field, 'LIKE', "%%%s%%" % txt]) + fields.append(field) if dimension_filters: if dimension_filters['allow_or_restrict'] == 'Allow': @@ -566,10 +571,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) query_filters.append(['name', query_selector, dimensions]) - output = frappe.get_list(doctype, filters=query_filters) - result = [d.name for d in output] + output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1) - return [(d,) for d in set(result)] + return [tuple(d) for d in set(output)] @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dad3ed70933..cc773b75963 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -120,13 +120,27 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if self.meta.get_field("commission_rate"): - self.round_floats_in(self, ["base_net_total", "commission_rate"]) - if self.commission_rate > 100.0: - throw(_("Commission rate cannot be greater than 100")) + if not self.meta.get_field("commission_rate"): + return - self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0, - self.precision("total_commission")) + self.round_floats_in( + self, ("amount_eligible_for_commission", "commission_rate") + ) + + if not (0 <= self.commission_rate <= 100.0): + throw("{} {}".format( + _(self.meta.get_label("commission_rate")), + _("must be between 0 and 100"), + )) + + self.amount_eligible_for_commission = sum( + item.base_net_amount for item in self.items if item.grant_commission + ) + + self.total_commission = flt( + self.amount_eligible_for_commission * self.commission_rate / 100.0, + self.precision("total_commission") + ) def calculate_contribution(self): if not self.meta.get_field("sales_team"): @@ -138,7 +152,7 @@ class SellingController(StockController): self.round_floats_in(sales_person) sales_person.allocated_amount = flt( - self.base_net_total * sales_person.allocated_percentage / 100.0, + self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, self.precision("allocated_amount", sales_person)) if sales_person.commission_rate: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae170945aa6..7073e32f536 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -134,7 +134,7 @@ class StockController(AccountsController): "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(sle.stock_value_difference, precision), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) @@ -143,7 +143,7 @@ class StockController(AccountsController): "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(sle.stock_value_difference, precision), "project": item_row.get("project") or self.get("project"), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No" diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js new file mode 100644 index 00000000000..c6569d8122e --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('CRM Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json new file mode 100644 index 00000000000..95b19fa9822 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "creation": "2021-09-09 17:03:22.754446", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "campaign_naming_by", + "allow_lead_duplication_based_on_emails", + "column_break_4", + "create_event_on_next_contact_date", + "auto_creation_of_contact", + "opportunity_section", + "close_opportunity_after_days", + "column_break_9", + "create_event_on_next_contact_date_opportunity", + "quotation_section", + "default_valid_till" + ], + "fields": [ + { + "fieldname": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Lead" + }, + { + "default": "0", + "fieldname": "allow_lead_duplication_based_on_emails", + "fieldtype": "Check", + "label": "Allow Lead Duplication based on Emails" + }, + { + "default": "1", + "fieldname": "auto_creation_of_contact", + "fieldtype": "Check", + "label": "Auto Creation of Contact" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "opportunity_section", + "fieldtype": "Section Break", + "label": "Opportunity" + }, + { + "default": "15", + "description": "Auto close Opportunity Replied after the no. of days mentioned above", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Replied Opportunity After Days" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date_opportunity", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "quotation_section", + "fieldtype": "Section Break", + "label": "Quotation" + } + ], + "icon": "fa fa-cog", + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "migration_hash": "3ae78b12dd1c64d551736c6e82092f90", + "modified": "2021-11-03 09:00:36.883496", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py new file mode 100644 index 00000000000..bde52547c95 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CRMSettings(Document): + pass diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py new file mode 100644 index 00000000000..3372c5deb4b --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestCRMSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c590523a4f8..9adbe8b6f1d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -11,6 +11,7 @@ from frappe.utils import ( cint, comma_and, cstr, + get_link_to_form, getdate, has_gravatar, nowdate, @@ -91,13 +92,14 @@ class Lead(SellingController): self.contact_doc.save() def add_calendar_event(self, opts=None, force=False): - super(Lead, self).add_calendar_event({ - "owner": self.lead_owner, - "starts_on": self.contact_date, - "ends_on": self.ends_on or "", - "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') - }, force) + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'): + super(Lead, self).add_calendar_event({ + "owner": self.lead_owner, + "starts_on": self.contact_date, + "ends_on": self.ends_on or "", + "subject": ('Contact ' + cstr(self.lead_name)), + "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') + }, force) def update_prospects(self): prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) @@ -108,12 +110,13 @@ class Lead(SellingController): def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) - duplicate_leads = [lead.name for lead in duplicate_leads] + if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'): + duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads] - if duplicate_leads: - frappe.throw(_("Email Address must be unique, already exists for {0}") - .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) + if duplicate_leads: + frappe.throw(_("Email Address must be unique, already exists for {0}") + .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) def on_trash(self): frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) @@ -172,41 +175,42 @@ class Lead(SellingController): self.title = self.company_name or self.lead_name def create_contact(self): - if not self.lead_name: - self.set_full_name() - self.set_lead_name() + if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'): + if not self.lead_name: + self.set_full_name() + self.set_lead_name() - contact = frappe.new_doc("Contact") - contact.update({ - "first_name": self.first_name or self.lead_name, - "last_name": self.last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.designation, - "company_name": self.company_name, - }) - - if self.email_id: - contact.append("email_ids", { - "email_id": self.email_id, - "is_primary": 1 + contact = frappe.new_doc("Contact") + contact.update({ + "first_name": self.first_name or self.lead_name, + "last_name": self.last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "company_name": self.company_name, }) - if self.phone: - contact.append("phone_nos", { - "phone": self.phone, - "is_primary_phone": 1 - }) + if self.email_id: + contact.append("email_ids", { + "email_id": self.email_id, + "is_primary": 1 + }) - if self.mobile_no: - contact.append("phone_nos", { - "phone": self.mobile_no, - "is_primary_mobile_no":1 - }) + if self.phone: + contact.append("phone_nos", { + "phone": self.phone, + "is_primary_phone": 1 + }) - contact.insert(ignore_permissions=True) + if self.mobile_no: + contact.append("phone_nos", { + "phone": self.mobile_no, + "is_primary_mobile_no":1 + }) - return contact + contact.insert(ignore_permissions=True) + + return contact @frappe.whitelist() def make_customer(source_name, target_doc=None): diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 0bef80a749d..fcbd4ded398 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import DocType from frappe.utils import cint, cstr, flt, get_fullname from erpnext.setup.utils import get_exchange_rate @@ -28,7 +29,6 @@ class Opportunity(TransactionBase): }) self.make_new_lead_if_required() - self.validate_item_details() self.validate_uom_is_integer("uom", "qty") self.validate_cust_name() @@ -70,21 +70,21 @@ class Opportunity(TransactionBase): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: # check if customer is already created agains the self.contact_email - customer = frappe.db.sql("""select - distinct `tabDynamic Link`.link_name as customer - from - `tabContact`, - `tabDynamic Link` - where `tabContact`.email_id='{0}' - and - `tabContact`.name=`tabDynamic Link`.parent - and - ifnull(`tabDynamic Link`.link_name, '')<>'' - and - `tabDynamic Link`.link_doctype='Customer' - """.format(self.contact_email), as_dict=True) - if customer and customer[0].customer: - self.party_name = customer[0].customer + dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact") + customer = frappe.qb.from_( + dynamic_link + ).join( + contact + ).on( + (contact.name == dynamic_link.parent) + & (dynamic_link.link_doctype == "Customer") + & (contact.email_id == self.contact_email) + ).select( + dynamic_link.link_name + ).distinct().run(as_dict=True) + + if customer and customer[0].link_name: + self.party_name = customer[0].link_name self.opportunity_from = "Customer" return @@ -191,30 +191,31 @@ class Opportunity(TransactionBase): self.add_calendar_event() def add_calendar_event(self, opts=None, force=False): - if not opts: - opts = frappe._dict() + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'): + if not opts: + opts = frappe._dict() - opts.description = "" - opts.contact_date = self.contact_date + opts.description = "" + opts.contact_date = self.contact_date - if self.party_name and self.opportunity_from == 'Customer': - if self.contact_person: - opts.description = 'Contact '+cstr(self.contact_person) - else: - opts.description = 'Contact customer '+cstr(self.party_name) - elif self.party_name and self.opportunity_from == 'Lead': - if self.contact_display: - opts.description = 'Contact '+cstr(self.contact_display) - else: - opts.description = 'Contact lead '+cstr(self.party_name) + if self.party_name and self.opportunity_from == 'Customer': + if self.contact_person: + opts.description = 'Contact '+cstr(self.contact_person) + else: + opts.description = 'Contact customer '+cstr(self.party_name) + elif self.party_name and self.opportunity_from == 'Lead': + if self.contact_display: + opts.description = 'Contact '+cstr(self.contact_display) + else: + opts.description = 'Contact lead '+cstr(self.party_name) - opts.subject = opts.description - opts.description += '. By : ' + cstr(self.contact_by) + opts.subject = opts.description + opts.description += '. By : ' + cstr(self.contact_by) - if self.to_discuss: - opts.description += ' To Discuss : ' + cstr(self.to_discuss) + if self.to_discuss: + opts.description += ' To Discuss : ' + cstr(self.to_discuss) - super(Opportunity, self).add_calendar_event(opts, force) + super(Opportunity, self).add_calendar_event(opts, force) def validate_item_details(self): if not self.get('items'): @@ -363,7 +364,7 @@ def set_multiple_status(names, status): def auto_close_opportunity(): """ auto close the `Replied` Opportunities after 7 days """ - auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 + auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and modified 0) { + frm.trigger("get_sub_assembly_items"); + } + } }); }, diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f4a88dc4598..f590d680d35 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -21,9 +21,10 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +from erpnext.tests.utils import ERPNextTestCase, timeout -class TestWorkOrder(unittest.TestCase): +class TestWorkOrder(ERPNextTestCase): def setUp(self): self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' @@ -376,6 +377,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(len(ste.additional_costs), 1) self.assertEqual(ste.total_additional_costs, 1000) + @timeout(seconds=60) def test_job_card(self): stock_entries = [] bom = frappe.get_doc('BOM', { @@ -769,6 +771,7 @@ class TestWorkOrder(unittest.TestCase): total_pl_qty ) + @timeout(seconds=60) def test_job_card_scrap_item(self): items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 647c14b33d9..4e1a464cb05 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -178,8 +178,9 @@ }, { "fieldname": "batch_size", - "fieldtype": "Int", - "label": "Batch Size" + "fieldtype": "Float", + "label": "Batch Size", + "read_only": 1 }, { "fieldname": "sequence_id", @@ -200,7 +201,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-24 04:52:54.295168", + "modified": "2021-11-29 16:37:18.824489", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index cf19cbf6a28..090a3e74fc8 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -89,7 +89,7 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) + details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get('parent'), {}) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js new file mode 100644 index 00000000000..b2428e85b74 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -0,0 +1,70 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Consumed Materials"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Work Order"), + fieldname: "name", + fieldtype: "Link", + options: "Work Order", + get_query: function() { + return { + filters: { + status: ["in", ["In Process", "Completed", "Stopped"]] + } + } + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "Link", + depends_on: "eval: !doc.name", + options: "Item" + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["In Process", "Completed", "Stopped"] + }, + { + label: __("Excess Materials Consumed"), + fieldname: "show_extra_consumed_materials", + fieldtype: "Check" + } + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) { + value = `
${value}
`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json new file mode 100644 index 00000000000..2fc986aa36a --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-11-22 17:36:11.886939", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2021-11-22 17:36:14.999091", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Consumed Materials", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Consumed Materials", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py new file mode 100644 index 00000000000..052834807e1 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -0,0 +1,131 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + fields = get_fields() + filters = get_filter_condition(report_filters) + + wo_items = {} + for d in frappe.get_all("Work Order", filters = filters, fields=fields): + d.extra_consumed_qty = 0.0 + if d.consumed_qty and d.consumed_qty > d.required_qty: + d.extra_consumed_qty = d.consumed_qty - d.required_qty + + if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials: + wo_items.setdefault((d.name, d.production_item), []).append(d) + + data = [] + for key, wo_data in wo_items.items(): + for index, row in enumerate(wo_data): + if index != 0: + #If one work order has multiple raw materials then show parent data in the first row only + for field in ["name", "status", "production_item", "qty", "produced_qty"]: + row[field] = "" + + data.append(row) + + return data + +def get_fields(): + return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code", + "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`", + "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`", + "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`", + "`tabWork Order`.`produced_qty`"] + +def get_filter_condition(report_filters): + filters = { + "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]), + "creation": ("between", [report_filters.from_date, report_filters.to_date]) + } + + for field in ["name", "production_item", "company", "status"]: + value = report_filters.get(field) + if value: + key = f"`{field}`" + filters.update({key: value}) + + return filters + +def get_columns(): + return [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 80 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Qty to Produce"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Raw Material Item"), + "fieldname": "raw_material_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 + }, + { + "label": _("Item Name"), + "fieldname": "raw_material_name", + "width": 130 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Transferred Qty"), + "fieldname": "transferred_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Extra Consumed Qty"), + "fieldname": "extra_consumed_qty", + "fieldtype": "Float", + "width": 100 + } + ] diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index cfa80f8e9fc..65b4d026395 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,10 +1,6 @@ { - "charts": [ - { - "chart_name": "Produced Quantity" - } - ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "charts": [], + "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -140,14 +136,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Work Order", "hidden": 0, @@ -295,9 +283,126 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Planning Report", + "link_count": 0, + "link_to": "Production Planning Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Work Order Summary", + "link_count": 0, + "link_to": "Work Order Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Quality Inspection", + "hidden": 0, + "is_query_report": 1, + "label": "Quality Inspection Summary", + "link_count": 0, + "link_to": "Quality Inspection Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Downtime Entry", + "hidden": 0, + "is_query_report": 1, + "label": "Downtime Analysis", + "link_count": 0, + "link_to": "Downtime Analysis", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Job Card", + "hidden": 0, + "is_query_report": 1, + "label": "Job Card Summary", + "link_count": 0, + "link_to": "Job Card Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Search", + "link_count": 0, + "link_to": "BOM Search", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Stock Report", + "link_count": 0, + "link_to": "BOM Stock Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Analytics", + "link_count": 0, + "link_to": "Production Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Operations Time", + "link_count": 0, + "link_to": "BOM Operations Time", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Work Order Consumed Materials", + "link_count": 0, + "link_to": "Work Order Consumed Materials", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-05 12:16:00.825742", + "modified": "2021-11-22 17:55:03.524496", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e475229125f..ee9060b37d6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -278,6 +278,7 @@ erpnext.patches.v13_0.update_tds_check_field #3 erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning +erpnext.patches.v13_0.remove_bad_selling_defaults erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.einvoicing_deprecation_warning @@ -287,7 +288,7 @@ erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.validate_options_for_data_field -erpnext.patches.v13_0.create_gst_payment_entry_fields +erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item @@ -312,3 +313,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.migrate_crm_settings diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index b3ef5af1007..5e1df14d4e0 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -3,9 +3,9 @@ import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field import erpnext -from erpnext.regional.india.setup import setup def execute(): @@ -30,7 +30,14 @@ def execute(): frappe.reload_doc('Regional', 'Report', report) if erpnext.get_region() == "India": - setup(patch=True) + create_custom_field('Salary Component', + dict(fieldname='component_type', + label='Component Type', + fieldtype='Select', + insert_after='description', + options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax', + depends_on='eval:doc.type == "Deduction"') + ) if frappe.db.exists("Salary Component", "Income Tax"): frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py index 7e6d67ce931..416694559cb 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -9,24 +9,29 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') frappe.reload_doc('accounts', 'doctype', 'payment_entry') - custom_fields = { - 'Payment Entry': [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) - ] - } + if frappe.db.exists('Company', {'country': 'India'}): + custom_fields = { + 'Payment Entry': [ + dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', + print_hide=1, collapsible=1), + dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', + print_hide=1, options='Address'), + dict(fieldname='company_gstin', label='Company GSTIN', + fieldtype='Data', insert_after='company_address', + fetch_from='company_address.gstin', print_hide=1, read_only=1), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='company_gstin', + print_hide=1, read_only=1), + dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', + print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), + dict(fieldname='customer_gstin', label='Customer GSTIN', + fieldtype='Data', insert_after='customer_address', + fetch_from='customer_address.gstin', print_hide=1, read_only=1) + ] + } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) + else: + fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + for field in fields: + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") \ No newline at end of file diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py new file mode 100644 index 00000000000..5487a6c60cc --- /dev/null +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -0,0 +1,15 @@ +import frappe +from frappe import _ + + +def execute(): + selling_settings = frappe.get_single("Selling Settings") + + if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"): + selling_settings.customer_group = None + + if selling_settings.territory in (_("All Territories"), "All Territories"): + selling_settings.territory = None + + selling_settings.flags.ignore_mandatory=True + selling_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py new file mode 100644 index 00000000000..30d3ea0cb1f --- /dev/null +++ b/erpnext/patches/v14_0/migrate_crm_settings.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [ + 'campaign_naming_by', + 'close_opportunity_after_days', + 'default_valid_till' + ], as_dict=True) + + frappe.reload_doc('crm', 'doctype', 'crm_settings') + frappe.db.set_value('CRM Settings', 'CRM Settings', { + 'campaign_naming_by': settings.campaign_naming_by, + 'close_opportunity_after_days': settings.close_opportunity_after_days, + 'default_valid_till': settings.default_valid_till + }) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index f563c08a047..476990a88e1 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -193,7 +193,7 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_list("Salary Detail", + component_and_amounts = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 78355cae645..93cba06da15 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -48,7 +48,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, @@ -84,7 +84,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 2cf9d38bd02..04156902f7f 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ import unittest import frappe -from frappe.utils import add_days, getdate, nowdate +from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -13,21 +13,26 @@ from erpnext.projects.report.project_profitability.project_profitability import class TestProjectProfitability(unittest.TestCase): - def setUp(self): + frappe.db.sql('delete from `tabTimesheet`') emp = make_employee('test_employee_9@salary.com', company='_Test Company') + if not frappe.db.exists('Salary Component', 'Timesheet Component'): frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert() + make_salary_structure_for_timesheet(emp, company='_Test Company') - self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) + date = getdate() + + self.timesheet = make_timesheet(emp, is_billable=1) self.salary_slip = make_salary_slip(self.timesheet.name) - holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate()) + + holidays = self.salary_slip.get_holidays_for_employee(date, date) if holidays: frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) self.salary_slip.submit() self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') - self.sales_invoice.due_date = nowdate() + self.sales_invoice.due_date = date self.sales_invoice.submit() frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) @@ -63,6 +68,4 @@ class TestProjectProfitability(unittest.TestCase): self.assertEqual(fractional_cost, row.fractional_cost) def tearDown(self): - frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() - frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() - frappe.get_doc("Timesheet", self.timesheet.name).cancel() + frappe.db.rollback() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0cfc008c13a..773d53c5521 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1106,7 +1106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe $.each(this.frm.doc.taxes || [], function(i, d) { if(d.charge_type == "Actual") { frappe.model.set_value(d.doctype, d.name, "tax_amount", - flt(d.tax_amount) / flt(exchange_rate)); + flt(d.base_tax_amount) / flt(exchange_rate)); } }); } diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 1677e9b3de4..7a3854cc611 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -495,6 +495,11 @@ font-size: var(--text-md); } + > .item-qty-total-container { + @extend .net-total-container; + padding: 5px 0px 0px 0px; + } + > .taxes-container { display: none; flex-direction: column; diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 9a72410f67b..0f0897841b4 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document): memberships = frappe.db.get_all('Membership', { 'member': self.member, 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'membership_status': ('!=', 'Cancelled') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 4bd91951916..fd3ec3c08ce 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -206,26 +206,17 @@ def get_regional_address_details(party_details, doctype, company): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) - - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.company_gstin: - return party_details + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.supplier_gstin: - return party_details + if tax_template_by_category: + party_details['taxes_and_charges'] = tax_template_by_category + return if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin @@ -237,6 +228,7 @@ def get_regional_address_details(party_details, doctype, company): if not default_tax: return party_details + party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) @@ -268,9 +260,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details): default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, 'name') - if default_tax: - party_details["taxes_and_charges"] = default_tax - party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + return default_tax def get_tax_template(master_doctype, company, is_inter_state, state_code): tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], @@ -569,17 +559,17 @@ def get_item_list(data, doc, hsn_wise=False): } item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) - for hsn_code, taxable_amount in hsn_taxable_amount.items(): + for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() - if not hsn_code: + if not item_or_hsn: frappe.throw(_('GST HSN Code does not exist for one or more items')) - item_data.hsnCode = int(hsn_code) + item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" for attr in item_data_attrs: item_data[attr] = 0 - for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items(): + for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): account_type = gst_accounts.get(account, '') for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: @@ -847,13 +837,11 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + depreciation_amount = (flt(asset.gross_purchase_amount) - + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 681f72fd309..8e9a72897df 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set tax_amount = frappe.utils.flt(data_object[item.item_code][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[item.item_code][0])-%}\n {{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-22 10:40:24.716932", + "modified": "2021-11-29 13:47:37.870818", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index 91a47674d7b..f3fe5e88488 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -106,14 +106,14 @@ def set_address_details(row, special_characters): row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_list("Sales Taxes and Charges", + taxes = frappe.get_all("Sales Taxes and Charges", filters={ 'parent': row.dn_id }, fields=('item_wise_tax_detail', 'account_head')) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_list("GST Account", + taxes_list = frappe.get_all("GST Account", filters={ "parent": "GST Settings", "company": filters.company diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 5a281a4cbb2..17e50648b3b 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -41,7 +41,7 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", + self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", filters = {"parent": self.filters.company}, pluck="account") if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index a2f634ee22c..7d00d8b3928 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,7 +1,10 @@ import io import os +from base64 import b64encode import frappe +from frappe import _ +from frappe.utils.data import add_to_date, get_time, getdate from pyqrcode import create as qr_create from erpnext import get_region @@ -28,28 +31,80 @@ def create_qr_code(doc, method): for field in meta.get_image_fields(): if field.fieldname == 'qr_code': - from urllib.parse import urlencode + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - # Creating public url to print format - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value") + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - # System Language - language = frappe.get_system_settings('language') + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) - params = urlencode({ - 'format': default_print_format or 'Standard', - '_lang': language, - 'key': doc.get_signature() - }) + tag = bytes([1]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Invoice Amount + invoice_amount = str(doc.grand_total) + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) + + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Joining bytes into one + tlv_buff = ''.join(tlv_array) + + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - # creating qr code for the url - url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?{ params }" qr_image = io.BytesIO() - url = qr_create(url, error='L') + url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) + name = frappe.generate_hash(doc.name, 5) + # making file - filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__") + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") _file = frappe.get_doc({ "doctype": "File", "file_name": filename, diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 2e2b8b77d39..0c8c53aabe3 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -18,7 +18,11 @@ from frappe.model.rename_doc import update_linked_doctypes from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.user import get_users_with_role -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( # noqa + get_dashboard_info, + get_timeline_data, + validate_party_accounts, +) from erpnext.utilities.transaction_base import TransactionBase diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ad788e5c8bf..ee5b0ea760a 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -961,9 +961,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "max_attachments": 1, - "migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e", - "modified": "2021-10-21 12:58:55.514512", + "modified": "2021-11-30 01:33:21.106073", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 769e0661b12..aa83726304f 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -302,6 +302,109 @@ class TestQuotation(unittest.TestCase): enable_calculate_bundle_price(enable=0) + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + + enable_calculate_bundle_price() + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.packed_items[0].rate = 100 + quotation.packed_items[1].rate = 200 + quotation.packed_items[2].rate = 200 + quotation.packed_items[3].rate = 300 + quotation.save() + + expected_values = [300, 500] + + for item in quotation.items: + self.assertEqual(item.amount, expected_values[item.idx-1]) + + enable_calculate_bundle_price(enable=0) + + def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Product Bundle 3", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", + ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 3", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + del quotation.items[1] + quotation.save() + + for id, item in enumerate(quotation.packed_items): + expected_index = id + 1 + self.assertEqual(item.idx, expected_index) + test_records = frappe.get_test_records('Quotation') def enable_calculate_bundle_price(enable=1): diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7c7ed9a9604..7e99a062439 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -134,6 +134,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1507,16 +1508,23 @@ "fieldtype": "Small Text", "label": "Dispatch Address", "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:09:51.515542", + "modified": "2021-10-05 12:16:40.775704", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 47b8ebd3485..e69e28da928 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -925,6 +925,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) @@ -977,6 +978,7 @@ def make_work_orders(items, sales_order, company, project=None): description=i['description'] )).insert() work_order.set_work_order_operations() + work_order.flags.ignore_mandatory = True work_order.save() out.append(work_order) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 1e5590e7489..95f6c4e96df 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -48,6 +48,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_24", "net_rate", "net_amount", @@ -789,15 +790,23 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:15:05.803091", + "modified": "2021-10-05 12:27:25.014789", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index c27f1ea81ad..27bc541d62f 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -11,11 +11,6 @@ "customer_group", "column_break_4", "territory", - "crm_settings_section", - "campaign_naming_by", - "default_valid_till", - "column_break_9", - "close_opportunity_after_days", "item_price_settings_section", "selling_price_list", "maintain_same_rate_action", @@ -43,13 +38,6 @@ "label": "Customer Naming By", "options": "Customer Name\nNaming Series\nAuto Name" }, - { - "fieldname": "campaign_naming_by", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Campaign Naming By", - "options": "Campaign Name\nNaming Series\nAuto Name" - }, { "fieldname": "customer_group", "fieldtype": "Link", @@ -71,18 +59,6 @@ "label": "Default Price List", "options": "Price List" }, - { - "default": "15", - "description": "Auto close Opportunity after the no. of days mentioned above", - "fieldname": "close_opportunity_after_days", - "fieldtype": "Int", - "label": "Close Opportunity After Days" - }, - { - "fieldname": "default_valid_till", - "fieldtype": "Data", - "label": "Default Quotation Validity Days" - }, { "fieldname": "column_break_5", "fieldtype": "Column Break" @@ -169,15 +145,6 @@ "fieldname": "column_break_4", "fieldtype": "Column Break" }, - { - "fieldname": "crm_settings_section", - "fieldtype": "Section Break", - "label": "CRM Settings" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, { "fieldname": "item_price_settings_section", "fieldtype": "Section Break", @@ -204,7 +171,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-08 19:38:10.175989", + "modified": "2021-09-13 12:32:17.004404", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index e7c5e769965..fb86e614b6c 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -8,7 +8,6 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document from frappe.utils import cint -from frappe.utils.nestedset import get_root_of class SellingSettings(Document): @@ -37,9 +36,3 @@ class SellingSettings(Document): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) - - def set_default_customer_group_and_territory(self): - if not self.customer_group: - self.customer_group = get_root_of('Customer Group') - if not self.territory: - self.territory = get_root_of('Territory') diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index a5b2d500414..4920584d95e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class { `
${this.get_discount_icon()} ${__('Add Discount')}
+
+
${__('Total Items')}
+
0.00
+
${__("Net Total")}
0.00
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class { this.$numpad_section.prepend( `
+
` @@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.net_total); + this.render_total_item_qty(frm.doc.items); const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); @@ -487,6 +493,21 @@ erpnext.PointOfSale.ItemCart = class { ); } + render_total_item_qty(items) { + var total_item_qty = 0; + items.map((item) => { + total_item_qty = total_item_qty + item.qty; + }); + + this.$totals_section.find('.item-qty-total-container').html( + `
${__('Total Quantity')}
${total_item_qty}
` + ); + + this.$numpad_section.find('.numpad-item-qty-total').html( + `
${__('Total Quantity')}: ${total_item_qty}
` + ); + } + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total-container').html( diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 20504789aa7..e2e0db40449 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -157,25 +157,19 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran commission_rate() { this.calculate_commission(); - refresh_field("total_commission"); } total_commission() { - if(this.frm.doc.base_net_total) { - frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]); + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); - if(this.frm.doc.base_net_total < this.frm.doc.total_commission) { - var msg = (__("[Error]") + " " + - __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission", - this.frm.doc.name)) + " > " + - __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name))); - frappe.msgprint(msg); - throw msg; - } + const { amount_eligible_for_commission } = this.frm.doc; + if(!amount_eligible_for_commission) return; - this.frm.set_value("commission_rate", - flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total)); - } + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); } allocated_percentage(doc, cdt, cdn) { @@ -185,7 +179,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran sales_person.allocated_percentage = flt(sales_person.allocated_percentage, precision("allocated_percentage", sales_person)); - sales_person.allocated_amount = flt(this.frm.doc.base_net_total * + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, precision("allocated_amount", sales_person)); refresh_field(["allocated_amount"], sales_person); @@ -259,28 +253,39 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } calculate_commission() { - if(this.frm.fields_dict.commission_rate) { - if(this.frm.doc.commission_rate > 100) { - var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) + - " " + __("cannot be greater than 100"); - frappe.msgprint(msg); - throw msg; - } + if(!this.frm.fields_dict.commission_rate) return; - this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0, - precision("total_commission")); + if(this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); } calculate_contribution() { var me = this; $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { frappe.model.round_floats_in(sales_person); - if(sales_person.allocated_percentage) { - sales_person.allocated_amount = flt( - me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0, - precision("allocated_amount", sales_person)); - } + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); }); } diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 95ca3867ee7..91f60fbd4e2 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -12,6 +12,10 @@ frappe.ui.form.on("Company", { } }); } + + frm.call('check_if_transactions_exist').then(r => { + frm.toggle_enable("default_currency", (!r.message)); + }); }, setup: function(frm) { erpnext.company.setup_queries(frm); @@ -87,9 +91,6 @@ frappe.ui.form.on("Company", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - frm.toggle_enable("default_currency", (frm.doc.__onload && - !frm.doc.__onload.transactions_exist)); - if (frappe.perm.has_perm("Cost Center", 0, 'read')) { frm.add_custom_button(__('Cost Centers'), function() { frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dedd2d3f55c..e7397394587 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -22,8 +22,8 @@ class Company(NestedSet): def onload(self): load_address_and_contact(self, "company") - self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist() + @frappe.whitelist() def check_if_transactions_exist(self): exists = False for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd015..98f91198853 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -303,7 +303,6 @@ def set_more_defaults(): def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") - selling_settings.set_default_customer_group_and_territory() selling_settings.cust_master_name = "Customer Name" selling_settings.so_required = "No" selling_settings.dn_required = "No" diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 1478007da83..cad4c54d7db 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -53,6 +53,7 @@ def before_tests(): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) enable_all_roles_and_domains() + set_defaults_for_tests() frappe.db.commit() @@ -127,6 +128,14 @@ def enable_all_roles_and_domains(): [d.name for d in domains]) add_all_roles_to('Administrator') +def set_defaults_for_tests(): + from frappe.utils.nestedset import get_root_of + + selling_settings = frappe.get_single("Selling Settings") + selling_settings.customer_group = get_root_of("Customer Group") + selling_settings.territory = get_root_of("Territory") + selling_settings.save() + def insert_record(records): for r in records: diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 1412acfcead..e47837f2ca4 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -10,7 +10,7 @@ "idx": 0, "label": "ERPNext Settings", "links": [], - "modified": "2021-10-26 21:32:55.323591", + "modified": "2021-11-05 21:32:55.323591", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -123,6 +123,13 @@ "label": "Products Settings", "link_to": "Products Settings", "type": "DocType" + }, + { + "doc_view": "", + "icon": "crm", + "label": "CRM Settings", + "link_to": "CRM Settings", + "type": "DocType" } ], "title": "ERPNext Settings" diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index ed8c878ad4a..0da45a54d56 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -22,7 +22,7 @@ def boot_session(bootinfo): 'customer_group') bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings', 'allow_stale')) - bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings', + bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings', 'default_valid_till')) # if no company, show a dialog box to create a new company diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 48b1cc53967..a33134b491a 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,7 +4,9 @@ import frappe from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.query_builder import Case +from frappe.query_builder.functions import Coalesce, Sum +from frappe.utils import flt class Bin(Document): @@ -19,34 +21,42 @@ class Bin(Document): - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) def get_first_sle(self): - sle = frappe.db.sql(""" - select * from `tabStock Ledger Entry` - where item_code = %s - and warehouse = %s - order by timestamp(posting_date, posting_time) asc, creation asc - limit 1 - """, (self.item_code, self.warehouse), as_dict=1) - return sle and sle[0] or None + sle = frappe.qb.DocType("Stock Ledger Entry") + first_sle = ( + frappe.qb.from_(sle) + .select("*") + .where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse)) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .limit(1) + ).run(as_dict=True) + + return first_sle and first_sle[0] or None def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' - self.reserved_qty_for_production = frappe.db.sql(''' - SELECT - CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - SUM(item.required_qty - item.transferred_qty) - ELSE - SUM(item.required_qty - item.consumed_qty) - END - FROM `tabWork Order` pro, `tabWork Order Item` item - WHERE - item.item_code = %s - and item.parent = pro.name - and pro.docstatus = 1 - and item.source_warehouse = %s - and pro.status not in ("Stopped", "Completed") - and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) - ''', (self.item_code, self.warehouse))[0][0] + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + self.reserved_qty_for_production = ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Case() + .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty)) + .else_(Sum(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == self.item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == self.warehouse) + & (wo.status.notin(["Stopped", "Completed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 self.set_projected_qty() @@ -55,36 +65,53 @@ class Bin(Document): def update_reserved_qty_for_sub_contracting(self): #reserved qty - reserved_qty_for_sub_contract = frappe.db.sql(''' - select ifnull(sum(itemsup.required_qty),0) - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup - where - itemsup.rm_item_code = %s - and itemsup.parent = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] - #Get Transferred Entries - materials_transferred = frappe.db.sql(""" - select - ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) - from - `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po - where - se.docstatus=1 - and se.purpose='Send to Subcontractor' - and ifnull(se.purchase_order, '') !='' - and (sed.item_code = %(item)s or sed.original_item = %(item)s) - and se.name = sed.parent - and se.purchase_order = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - """, {'item': self.item_code})[0][0] + po = frappe.qb.DocType("Purchase Order") + supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") + + reserved_qty_for_sub_contract = ( + frappe.qb + .from_(po) + .from_(supplied_item) + .select(Sum(Coalesce(supplied_item.required_qty, 0))) + .where( + (supplied_item.rm_item_code == self.item_code) + & (po.name == supplied_item.parent) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + & (supplied_item.reserve_warehouse == self.warehouse) + ) + ).run()[0][0] or 0.0 + + se = frappe.qb.DocType("Stock Entry") + se_item = frappe.qb.DocType("Stock Entry Detail") + + materials_transferred = ( + frappe.qb + .from_(se) + .from_(se_item) + .from_(po) + .select(Sum( + Case() + .when(se.is_return == 1, se_item.transfer_qty * -1) + .else_(se_item.transfer_qty) + )) + .where( + (se.docstatus == 1) + & (se.purpose == "Send to Subcontractor") + & (Coalesce(se.purchase_order, "") != "") + & ((se_item.item_code == self.item_code) + | (se_item.original_item == self.item_code)) + & (se.name == se_item.parent) + & (po.name == se.purchase_order) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + ) + ).run()[0][0] or 0.0 if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred @@ -100,33 +127,11 @@ def on_doctype_update(): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): - '''Called from erpnext.stock.utils.update_bin''' + """WARNING: This function is deprecated. Inline this function instead of using it.""" + from erpnext.stock.stock_ledger import repost_current_voucher + update_qty(bin_name, args) - - if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle - - if not args.get("posting_date"): - args["posting_date"] = nowdate() - - if args.get("is_cancelled") and via_landed_cost_voucher: - return - - # Reposts only current voucher SL Entries - # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('warehouse'), - "posting_date": args.get("posting_date"), - "posting_time": args.get("posting_time"), - "voucher_type": args.get("voucher_type"), - "voucher_no": args.get("voucher_no"), - "sle_id": args.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - - # update qty in future sle and Validate negative qty - update_qty_in_future_sle(args, allow_negative_stock) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) def get_bin_details(bin_name): return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', @@ -160,4 +165,4 @@ def update_qty(bin_name, args): 'indented_qty': indented_qty, 'planned_qty': planned_qty, 'projected_qty': projected_qty - }) \ No newline at end of file + }) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ad1b3b43aee..55a4c956a67 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -145,6 +145,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1302,16 +1303,23 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-10-08 14:29:13.428984", + "modified": "2021-10-09 14:29:13.428984", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a96c29925e5..51c88bed61d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_25", "net_rate", "net_amount", @@ -753,13 +754,20 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:12:44.018872", + "modified": "2021-10-06 12:12:44.018872", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4b314a00a42..4f4e69105a2 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -88,6 +88,7 @@ "sales_details", "sales_uom", "is_sales_item", + "grant_commission", "column_break3", "max_discount", "deferred_revenue", @@ -1020,6 +1021,12 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" + }, + { + "default": "1", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission" } ], "has_web_view": 1, @@ -1028,8 +1035,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "max_attachments": 1, - "modified": "2021-10-27 21:04:00.324786", + "modified": "2021-12-03 08:32:03.869294", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1097,7 +1103,7 @@ "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, - "sort_field": "idx desc,modified desc", + "sort_field": "modified", "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5daabe817b2..decf522d2f7 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -222,10 +222,11 @@ class Item(WebsiteGenerator): 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) def validate_website_image(self): + """Validate if the website image is a public file""" + if frappe.flags.in_import: return - """Validate if the website image is a public file""" auto_set_website_image = False if not self.website_image and self.image: auto_set_website_image = True @@ -255,10 +256,11 @@ class Item(WebsiteGenerator): self.website_image = None def make_thumbnail(self): + """Make a thumbnail of `website_image`""" + if frappe.flags.in_import: return - """Make a thumbnail of `website_image`""" import requests.exceptions if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): @@ -722,7 +724,6 @@ class Item(WebsiteGenerator): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -736,7 +737,6 @@ class Item(WebsiteGenerator): repost_stock(new_name, warehouse) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 @frappe.whitelist() def copy_specification_from_item_group(self): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 7237178b15b..4028d933341 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -488,7 +488,7 @@ class TestItem(ERPNextTestCase): item_doc.save() # Check values saved correctly - barcodes = frappe.get_list( + barcodes = frappe.get_all( 'Item Barcode', fields=['barcode', 'barcode_type'], filters={'parent': item_code}) @@ -534,8 +534,6 @@ class TestItem(ERPNextTestCase): def test_index_creation(self): "check if index is getting created in db" - from erpnext.stock.doctype.item.item import on_doctype_update - on_doctype_update() indices = frappe.db.sql("show index from tabItem", as_dict=1) expected_columns = {"item_code", "item_name", "item_group", "route"} diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3f73093d673..e4091c40dc4 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -108,9 +108,32 @@ def cleanup_packing_list(doc, parent_items): packed_items = doc.get("packed_items") doc.set("packed_items", []) + for d in packed_items: if d not in delete_list: - doc.append("packed_items", d) + add_item_to_packing_list(doc, d) + +def add_item_to_packing_list(doc, packed_item): + doc.append("packed_items", { + 'parent_item': packed_item.parent_item, + 'item_code': packed_item.item_code, + 'item_name': packed_item.item_name, + 'uom': packed_item.uom, + 'qty': packed_item.qty, + 'rate': packed_item.rate, + 'conversion_factor': packed_item.conversion_factor, + 'description': packed_item.description, + 'warehouse': packed_item.warehouse, + 'batch_no': packed_item.batch_no, + 'actual_batch_qty': packed_item.actual_batch_qty, + 'serial_no': packed_item.serial_no, + 'target_warehouse': packed_item.target_warehouse, + 'actual_qty': packed_item.actual_qty, + 'projected_qty': packed_item.projected_qty, + 'incoming_rate': packed_item.incoming_rate, + 'prevdoc_doctype': packed_item.prevdoc_doctype, + 'parent_detail_docname': packed_item.parent_detail_docname + }) def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" @@ -128,7 +151,8 @@ def update_product_bundle_price(doc, parent_items): else: update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) - bundle_price = 0 + bundle_item_rate = bundle_item.rate if bundle_item.rate else 0 + bundle_price = bundle_item.qty * bundle_item_rate parent_items_index += 1 # for the last product bundle diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 965a32d92d8..01cceb176b2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -54,9 +54,11 @@ class RepostItemValuation(Document): @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued') - frappe.enqueue(repost, timeout=1800, queue='long', - job_name='repost_sle', now=True, doc=self) + self.set_status('Queued', write=False) + self.current_index = 0 + self.distinct_item_and_warehouse = None + self.items_to_be_repost = None + self.db_update() def deduplicate_similar_repost(self): """ Deduplicate similar reposts based on item-warehouse-posting combination.""" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d31e65a4cc5..a38dfa5062b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -545,7 +545,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 067946785a9..5a9e77e325e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -24,7 +24,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle +from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -38,9 +39,10 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(unittest.TestCase): +class TestStockEntry(ERPNextTestCase): def tearDown(self): frappe.set_user("Administrator") + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -582,6 +584,65 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + def test_work_order_manufacture_with_material_consumption(self): + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as _make_stock_entry, + ) + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") + + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", + "is_default": 1, "docstatus": 1}) + + work_order = frappe.new_doc("Work Order") + work_order.update({ + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC" + }) + work_order.insert() + work_order.submit() + + make_stock_entry(item_code="_Test Item", + target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry(item_code="_Test Item Home Desktop 100", + target="Stores - _TC", qty=10, basic_rate=1000.0) + + + s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + for d in s.get("items"): + d.s_warehouse = "Stores - _TC" + s.insert() + s.submit() + + # When Stock Entry has RM and FG + s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) + s.save() + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount + self.assertEqual(fg_cost, + flt(rm_cost - scrap_cost, 2)) + + # When Stock Entry has only FG + Scrap + s.items.pop(0) + s.items.pop(0) + s.submit() + + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + self.assertEqual(rm_cost, 0) + expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", @@ -868,6 +929,83 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle(self): + # Initialize item, batch, warehouse, opening qty + item_code = '_Test Future Neg Item' + batch_no = '_Test Future Neg Batch' + warehouses = [ + '_Test Future Neg Warehouse Source', + '_Test Future Neg Warehouse Destination' + ] + warehouse_names = initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, + opening_qty=2, posting_date='2021-07-01' + ) + + # Executing an illegal sequence should raise an error + sequence_of_entries = [ + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-03', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[1], + to_warehouse=warehouse_names[0], + batch_no=batch_no, + posting_date='2021-07-04', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-02', # Illegal SE + purpose='Material Transfer') + ] + + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) + + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle_batch(self): + from erpnext.stock.doctype.batch.test_batch import TestBatch + + # Initialize item, batch, warehouse, opening qty + item_code = '_Test MultiBatch Item' + TestBatch.make_batch_item(item_code) + + batch_nos = [] # store generate batches + warehouse = '_Test Warehouse - _TC' + + se1 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-01', + purpose='Material Receipt' + ) + batch_nos.append(se1.items[0].batch_no) + se2 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-03', + purpose='Material Receipt' + ) + batch_nos.append(se2.items[0].batch_no) + + with self.assertRaises(NegativeStockError) as nse: + make_stock_entry(item_code=item_code, + qty=1, + from_warehouse=warehouse, + batch_no=batch_nos[1], + posting_date='2021-09-02', # backdated consumption of 2nd batch + purpose='Material Issue') + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -938,3 +1076,31 @@ def get_multiple_items(): ] test_records = frappe.get_test_records('Stock Entry') + +def initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, opening_qty, posting_date): + from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + TestBatch.make_batch_item(item_code) + make_new_batch(item_code=item_code, batch_id=batch_no) + warehouse_names = [create_warehouse(w) for w in warehouses] + create_stock_reconciliation( + purpose='Opening Stock', + posting_date=posting_date, + posting_time='20:00:20', + item_code=item_code, + warehouse=warehouse_names[0], + valuation_rate=100, + qty=opening_qty, + batch_no=batch_no, + ) + return warehouse_names + + +def create_stock_entries(sequence_of_entries): + for entry_detail in sequence_of_entries: + make_stock_entry(**entry_detail) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 93bca7a6947..c53830799d4 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -43,7 +43,6 @@ class StockLedgerEntry(Document): def on_submit(self): self.check_stock_frozen_date() - self.actual_amt_check() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): @@ -57,18 +56,6 @@ class StockLedgerEntry(Document): "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) - def actual_amt_check(self): - """Validate that qty at warehouse for selected batch is >=0""" - if self.batch_no and not self.get("allow_negative_stock"): - batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", - (self.warehouse, self.item_code, self.batch_no))[0][0]) - - if batch_bal_after_transaction < 0: - frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}") - .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse)) - def validate_mandatory(self): mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] for k in mandatory: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index b7d1497319f..3402972bb89 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-03-28 10:35:31", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", @@ -153,11 +154,12 @@ "icon": "fa fa-upload-alt", "idx": 1, "is_submittable": 1, - "max_attachments": 1, - "modified": "2020-04-08 17:02:47.196206", + "links": [], + "modified": "2021-11-30 01:33:51.437194", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index ca92936a1dc..26db2642e4b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) - def test_warehouse_renaming(self): - create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") - account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") - self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) - - # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") - - def test_warehouse_merging(self): - company = "_Test Company with perpetual inventory" - create_warehouse("Test Warehouse for Merging 1", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 2", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", - qty=1, rate=100, company=company) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", - qty=1, rate=100, company=company) - - existing_bin_qty = ( - cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) - + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) - ) - - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", - "Test Warehouse for Merging 2 - TCP1", merge=True) - - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) - - bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") - - self.assertEqual(bin_qty, existing_bin_qty) - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - TCP1"})) - def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 9b9093261c2..05076b51a3e 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "creation": "2013-03-07 18:50:32", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", @@ -245,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-04-09 19:54:56.263965", + "modified": "2021-12-03 04:40:06.414630", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index b9dbc388805..9cfad86f142 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet -import erpnext from erpnext.stock import get_warehouse_account @@ -68,57 +67,6 @@ class Warehouse(NestedSet): return frappe.db.sql("""select name from `tabWarehouse` where parent_warehouse = %s limit 1""", self.name) - def before_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).before_rename(old_name, new_name, merge) - - # Add company abbr if not provided - new_warehouse = erpnext.encode_company_abbr(new_name, self.company) - - if merge: - if not frappe.db.exists("Warehouse", new_warehouse): - frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse)) - - if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"): - frappe.throw(_("Both Warehouse must belong to same Company")) - - return new_warehouse - - def after_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).after_rename(old_name, new_name, merge) - - new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name) - self.db_set("warehouse_name", new_warehouse_name) - - if merge: - self.recalculate_bin_qty(new_name) - - def get_new_warehouse_name_without_abbr(self, name): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") - parts = name.rsplit(" - ", 1) - - if parts[-1].lower() == company_abbr.lower(): - name = parts[0] - - return name - - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - repost_stock_for_items = frappe.db.sql_list("""select distinct item_code - from tabBin where warehouse=%s""", new_name) - - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name) - - for item_code in repost_stock_for_items: - repost_stock(item_code, new_name) - - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 - def convert_to_group_or_ledger(self): if self.is_group: self.convert_to_ledger() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e00382bec1a..9889a22b35c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -299,7 +299,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": None or get_default_discount_account(args, item_defaults), + "discount_account": get_default_discount_account(args, item_defaults), "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, @@ -317,6 +317,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "net_rate": 0.0, "net_amount": 0.0, "discount_percentage": 0.0, + "discount_amount": 0.0, "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, @@ -326,7 +327,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index d3af5f61e33..4d1491b1b55 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -46,8 +46,8 @@ def execute(filters=None): item_balance.setdefault((item, item_map[item]["item_group"]), []) total_stock_value = 0.00 for wh in warehouse_list: - row += [qty_dict.bal_qty] if wh.name in warehouse else [0.00] - total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00 + row += [qty_dict.bal_qty] if wh.name == warehouse else [0.00] + total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) item_value.setdefault((item, item_map[item]["item_group"]),[]) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9d409827db1..e95c0fcd23b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,9 +7,10 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext +from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -17,19 +18,15 @@ from erpnext.stock.utils import ( ) -# future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') -# _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: - from erpnext.stock.utils import update_bin - cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) @@ -64,7 +61,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - update_bin(args, allow_negative_stock, via_landed_cost_voucher) + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + if is_stock_item: + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_bin_qty(bin_name, args) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) + else: + frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + +def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": + if not args.get("posting_date"): + args["posting_date"] = nowdate() + + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction + update_entries_after({ + "item_code": args.get('item_code'), + "warehouse": args.get('warehouse'), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get('name'), + "creation": args.get('creation') + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + # update qty in future sle and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def get_args_for_future_sle(row): return frappe._dict({ @@ -803,9 +831,9 @@ class update_entries_after(object): def update_bin(self): # update bin for each warehouse for warehouse, data in self.data.items(): - bin_record = get_or_make_bin(self.item_code, warehouse) + bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_record, { + frappe.db.set_value('Bin', bin_name, { "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value @@ -1061,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: - sle = get_future_sle_with_negative_qty(args) - if sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - sle[0]["posting_date"], sle[0]["posting_time"], - frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) + if allow_negative_stock: + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + return + + neg_sle = get_future_sle_with_negative_qty(args) + if neg_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) + + frappe.throw(message, NegativeStockError, title='Insufficient Stock') + + + if not args.batch_no: + return + + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if neg_batch_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link('Batch', args.batch_no), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1090,6 +1137,29 @@ def get_future_sle_with_negative_qty(args): limit 1 """, args, as_dict=1) + +def get_future_sle_with_negative_batch_qty(args): + return frappe.db.sql(""" + with batch_ledger as ( + select + posting_date, posting_time, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by posting_date, posting_time, creation + ) + select * from batch_ledger + where + cumulative_total < 0.0 + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + limit 1 + """, args, as_dict=1) + + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 6. diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8031c58b812..72d8098d440 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -187,7 +187,7 @@ def get_bin(item_code, warehouse): bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code, warehouse) -> str: +def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: @@ -203,11 +203,12 @@ def get_or_make_bin(item_code, warehouse) -> str: return bin_record def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): + """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: - bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) - update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 91df5480e35..fbf25948a79 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import signal import unittest from contextlib import contextmanager from typing import Any, Dict, NewType, Optional @@ -135,3 +136,23 @@ def execute_script_report( report_execute_fn(filter_with_optional_param) return report_data + + +def timeout(seconds=30, error_message="Test timed out."): + """ Timeout decorator to ensure a test doesn't run for too long. + + adapted from https://stackoverflow.com/a/2282656""" + def decorator(func): + def _handle_timeout(signum, frame): + raise Exception(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + return wrapper + return decorator