diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index f28a07431fe..88e1055beb4 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname): for col in column_list: sanitize_searchfield(col) return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' - .format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0] + .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py index 1318cf18d76..b8f1b85907f 100644 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py +++ b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py @@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document): def populate_payment_entries(self): if self.bank_statement is None: return - filename = self.bank_statement.split("/")[-1] + file_url = self.bank_statement if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): frappe.throw(_("Transactions already retreived from the statement")) @@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document): if self.bank_settings: mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items statement_headers = self.get_statement_headers() - transactions = get_transaction_entries(filename, statement_headers) + transactions = get_transaction_entries(file_url, statement_headers) for entry in transactions: date = entry[statement_headers["Date"]].strip() #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) @@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row): transaction[header] = "" return transaction -def get_transaction_entries(filename, headers): +def get_transaction_entries(file_url, headers): header_index = {} rows, transactions = [], [] - if (filename.lower().endswith("xlsx")): + if (file_url.lower().endswith("xlsx")): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_id=filename) - elif (filename.lower().endswith("csv")): + rows = read_xlsx_file_from_attached_file(file_url=file_url) + elif (file_url.lower().endswith("csv")): from frappe.utils.csvutils import read_csv_content - _file = frappe.get_doc("File", {"file_name": filename}) + _file = frappe.get_doc("File", {"file_url": file_url}) filepath = _file.get_full_path() with open(filepath,'rb') as csvfile: rows = read_csv_content(csvfile.read()) - elif (filename.lower().endswith("xls")): + elif (file_url.lower().endswith("xls")): + filename = file_url.split("/")[-1] rows = get_rows_from_xls_file(filename) else: frappe.throw(_("Only .csv and .xlsx files are supported currently")) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 3604b60b751..d37d76f8880 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -619,20 +619,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i
\ If this is undesirable please cancel the corresponding Payment Entry.") @@ -340,7 +340,7 @@ class PaymentEntry(AccountsController): frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) - def set_status(self): + def set_status(self, update=False): if self.docstatus == 2: self.status = 'Cancelled' elif self.docstatus == 1: @@ -348,6 +348,9 @@ class PaymentEntry(AccountsController): else: self.status = 'Draft' + if update: + self.db_set('status', self.status) + def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index f8d52a78336..83a4e921743 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") -def make_pos_profile(): +def make_pos_profile(**args): frappe.db.sql("delete from `tabPOS Profile`") + args = frappe._dict(args) + pos_profile = frappe.get_doc({ - "company": "_Test Company", - "cost_center": "_Test Cost Center - _TC", - "currency": "INR", + "company": args.company or "_Test Company", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "currency": args.currency or "INR", "doctype": "POS Profile", - "expense_account": "_Test Account Cost for Goods Sold - _TC", - "income_account": "Sales - _TC", - "name": "_Test POS Profile", + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "income_account": args.income_account or "Sales - _TC", + "name": args.name or "_Test POS Profile", "naming_series": "_T-POS Profile-", - "selling_price_list": "_Test Price List", - "territory": "_Test Territory", + "selling_price_list": args.selling_price_list or "_Test Price List", + "territory": args.territory or "_Test Territory", "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'), - "warehouse": "_Test Warehouse - _TC", - "write_off_account": "_Test Write Off - _TC", - "write_off_cost_center": "_Test Write Off Cost Center - _TC" + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "write_off_account": args.write_off_account or "_Test Write Off - _TC", + "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" }) - if not frappe.db.exists("POS Profile", "_Test POS Profile"): + if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): pos_profile.insert() return pos_profile diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 4a8fcc03fd1..b35e32c5ca5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,7 +13,8 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + 'Sales Order': ['items', 'sales_order'], + 'Delivery Note': ['items', 'delivery_note'] }, 'transactions': [ { @@ -33,4 +34,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f1a2bf7aa0e..26f488d8c9a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -714,6 +714,64 @@ class TestSalesInvoice(unittest.TestCase): self.pos_gl_entry(si, pos, 50) + def test_pos_returns_without_repayment(self): + pos_profile = make_pos_profile() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + + pos_return.insert() + pos_return.submit() + + self.assertFalse(pos_return.is_pos) + self.assertFalse(pos_return.get('payments')) + + def test_pos_returns_with_repayment(self): + pos_profile = make_pos_profile() + + pos_profile.append('payments', { + 'default': 1, + 'mode_of_payment': 'Cash', + 'amount': 0.0 + }) + + pos_profile.save() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + pos_return.insert() + pos_return.submit() + + self.assertEqual(pos_return.get('payments')[0].amount, -500) + pos_profile.payments = [] + pos_profile.save() + + def test_pos_change_amount(self): make_pos_profile() diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 32b97ba80b5..c17f3aeb846 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "ACC-SUB-.YYYY.-.#####", "creation": "2017-07-18 17:50:43.967266", "doctype": "DocType", @@ -184,7 +183,8 @@ "fieldname": "invoices", "fieldtype": "Table", "label": "Invoices", - "options": "Subscription Invoice" + "options": "Subscription Invoice", + "read_only": 1 }, { "collapsible": 1, @@ -197,8 +197,7 @@ "fieldtype": "Column Break" } ], - "links": [], - "modified": "2020-01-27 14:37:32.845173", + "modified": "2020-08-27 23:30:02.504042", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 98d07c71a3a..bc34816e375 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -326,8 +326,7 @@ class Subscription(Document): def is_postpaid_to_invoice(self): return getdate(nowdate()) > getdate(self.current_invoice_end) or \ - (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \ - not self.has_outstanding_invoice() + (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) def is_prepaid_to_invoice(self): if not self.generate_invoice_at_period_start: @@ -337,8 +336,16 @@ class Subscription(Document): return True # Check invoice dates and make sure it doesn't have outstanding invoices - return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice() - + return getdate(nowdate()) >= getdate(self.current_invoice_start) + + def is_current_invoice_generated(self): + invoice = self.get_current_invoice() + + if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end): + return True + + return False + def is_current_invoice_paid(self): if self.is_new_subscription(): return False @@ -346,7 +353,7 @@ class Subscription(Document): last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice) if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid': return True - + return False def process_for_active(self): @@ -358,7 +365,8 @@ class Subscription(Document): 2. Change the `Subscription` status to 'Past Due Date' 3. Change the `Subscription` status to 'Cancelled' """ - if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if not self.is_current_invoice_generated() and not self.is_current_invoice_paid() and \ + (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): self.generate_invoice() if self.current_invoice_is_past_due(): self.status = 'Past Due Date' @@ -369,6 +377,9 @@ class Subscription(Document): if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end): self.cancel_subscription_at_period_end() + if self.is_current_invoice_generated() and getdate() > getdate(self.current_invoice_end): + self.update_subscription_period(add_days(self.current_invoice_end, 1)) + def cancel_subscription_at_period_end(self): """ Called when `Subscription.cancel_at_period_end` is truthy diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 3d96f233b40..e38de252699 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -101,19 +101,19 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_invoice_is_generated_at_end_of_billing_period(self): + start_date = add_to_date(nowdate(), months=-1) subscription = frappe.new_doc('Subscription') subscription.customer = '_Test Customer' - subscription.start = '2018-01-01' + subscription.start = start_date subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) subscription.insert() self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, '2018-01-01') - self.assertEqual(subscription.current_invoice_end, '2018-01-31') + self.assertEqual(subscription.current_invoice_start, start_date) + self.assertEqual(subscription.current_invoice_end, add_days(nowdate(), -1)) subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, '2018-01-01') self.assertEqual(subscription.status, 'Past Due Date') subscription.delete() @@ -137,7 +137,6 @@ class TestSubscription(unittest.TestCase): subscription.process() self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1)) self.assertEqual(len(subscription.invoices), 1) subscription.delete() @@ -538,3 +537,23 @@ class TestSubscription(unittest.TestCase): settings.save() subscription.delete() + + def test_duplicate_invoice_check(self): + subscription = frappe.new_doc('Subscription') + subscription.customer = '_Test Customer' + subscription.generate_invoice_at_period_start = True + subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.start = nowdate() + subscription.save() + + # Generate invoice for the current invoicing period + subscription.process() + subscription.load_from_db() + self.assertEqual(len(subscription.invoices), 1) + + # Proccess subscription again for the same period + subscription.process() + subscription.load_from_db() + + # No new invoice should be created for current period + self.assertEqual(len(subscription.invoices), 1) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 1ed3f2341eb..cecf7f5e6fc 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1064,6 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $(frappe.render_template("pos_item", { item_code: escape(obj.name), item_price: item_price, + title: obj.name || obj.item_name, item_name: obj.name === obj.item_name ? "" : obj.item_name, item_image: obj.image, item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj), @@ -1545,6 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $.each(this.frm.doc.items || [], function (i, d) { $(frappe.render_template("pos_bill_item_new", { item_code: escape(d.item_code), + title: d.item_code || d.item_name, item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name), qty: d.qty, discount_percentage: d.discount_percentage || 0.0, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b0b64c8b56f..12e7b8b8c37 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -391,7 +391,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup from erpnext.accounts.doctype.tax_rule.tax_rule import get_tax_template, get_party_details args = { party_type.lower(): party, - "company": company + "company": company } if tax_category: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 58117b68c52..9e3f3b739c2 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -14,7 +14,7 @@ import frappe, erpnext from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency from erpnext.accounts.utils import get_fiscal_year from frappe import _ -from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr) +from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint) from six import itervalues from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children @@ -43,7 +43,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, periodicity, accumulated_v start_date = year_start_date months = get_months(year_start_date, year_end_date) - for i in range(math.ceil(months / months_to_add)): + for i in range(cint(math.ceil(months / months_to_add))): period = frappe._dict({ "from_date": start_date }) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index bb9cfcd886f..ec007061bfd 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -43,8 +43,11 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) + + if not filters.get("from_date") and not filters.get("to_date"): + frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 9cfb0627d30..cd6bac2d77d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,24 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-02-25 17:03:34", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:12:22.464240", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Gross Profit", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "Gross Profit", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2013-02-25 17:03:34", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-08-13 11:26:39.112352", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Gross Profit", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "Gross Profit", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index bfb44e5c30a..2a9e4db32b9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -160,7 +160,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(nowdate()), + 'transaction_date': getdate(self.purchase_date), 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 5dacfb0e02e..ee07892fe43 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1055,7 +1055,8 @@ "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, - "modified": "2020-07-01 12:40:45.240948", + "links": [], + "modified": "2020-09-14 14:36:12.418690", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1112,5 +1113,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "supplier", - "title_field": "title" + "title_field": "supplier", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 075db6e46ba..86e1e973abb 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError +from erpnext.stock.doctype.batch.test_batch import make_new_batch +from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -580,7 +582,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_exploded_items_in_subcontracted(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") @@ -602,7 +604,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_backflush_based_on_stock_entry(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) make_item('Sub Contracted Raw Material 1', { 'is_stock_item': 1, 'is_sub_contracted_item': 1 @@ -661,6 +663,76 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") + def test_backflushed_based_on_for_multiple_batches(self): + item_code = "_Test Subcontracted FG Item 2" + make_item('Sub Contracted Raw Material 2', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) + + make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, + raw_materials=["Sub Contracted Raw Material 2"]) + + update_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", + "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.submit() + + for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: + make_new_batch(batch_id=batch, item_code=item_code) + + pr = make_purchase_receipt(po.name) + + # partial receipt + pr.get('items')[0].qty = 30 + pr.get('items')[0].batch_no = "ABCD1" + + purchase_order = po.name + purchase_order_item = po.items[0].name + + for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): + pr.append("items", { + "item_code": pr.get('items')[0].item_code, + "item_name": pr.get('items')[0].item_name, + "uom": pr.get('items')[0].uom, + "stock_uom": pr.get('items')[0].stock_uom, + "warehouse": pr.get('items')[0].warehouse, + "conversion_factor": pr.get('items')[0].conversion_factor, + "cost_center": pr.get('items')[0].cost_center, + "rate": pr.get('items')[0].rate, + "qty": qty, + "batch_no": batch_no, + "purchase_order": purchase_order, + "purchase_order_item": purchase_order_item + }) + + pr.submit() + + pr1 = make_purchase_receipt(po.name) + pr1.get('items')[0].qty = 300 + pr1.get('items')[0].batch_no = "ABCD1" + pr1.save() + + pr_key = ("Sub Contracted Raw Material 2", po.name) + consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) + + self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) + self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) + + update_backflush_based_on("BOM") + def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry frappe.db.set_value("Accounts Settings", "Accounts Settings", @@ -712,27 +784,33 @@ def make_pr_against_po(po, received_qty=0): pr.submit() return pr -def make_subcontracted_item(item_code): +def make_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - if not frappe.db.exists('Item', item_code): - make_item(item_code, { + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { 'is_stock_item': 1, - 'is_sub_contracted_item': 1 + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 }) - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) - if not frappe.db.get_value('BOM', {'item': item_code}, 'name'): - make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1']) + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) def update_backflush_based_on(based_on): doc = frappe.get_doc('Buying Settings') diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4f34f7f3ce4..fc535074005 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json from frappe import _, throw from frappe.utils import (today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) +from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency @@ -1163,7 +1164,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item -def check_and_delete_children(parent, data): +def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -1190,18 +1191,37 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): - def check_permissions(doc, perm_type='create'): + def check_doc_permissions(doc, perm_type='create'): try: doc.check_permission(perm_type) - except: - action = "add" if perm_type == 'create' else "update" - frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + except frappe.PermissionError: + actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + + frappe.throw(_("You do not have permissions to {} items in a {}.") + .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) + + def validate_workflow_conditions(doc): + workflow = get_workflow_name(doc.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = doc.get(workflow_doc.workflow_state_field) + roles = frappe.get_roles() + + transitions = [] + for transition in workflow_doc.transitions: + if transition.next_state == current_state and transition.allowed in roles: + if not is_transition_condition_satisfied(transition, doc): + continue + transitions.append(transition.as_dict()) + + if not transitions: + frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) def get_new_child_item(item_row): - if parent_doctype == "Sales Order": - return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) - if parent_doctype == "Purchase Order": - return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults + return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): @@ -1214,17 +1234,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - - check_and_delete_children(parent, data) + + check_doc_permissions(parent, 'cancel') + validate_and_delete_children(parent, data) for d in data: new_child_flag = False if not d.get("docname"): new_child_flag = True - check_permissions(parent, 'create') + check_doc_permissions(parent, 'create') child_item = get_new_child_item(d) else: - check_permissions(parent, 'write') + check_doc_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) @@ -1331,6 +1352,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_prevdoc_status('submit') parent.update_delivery_status() + parent.reload() + validate_workflow_conditions(parent) + parent.update_blanket_order() parent.update_billing_percentage() parent.set_status() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c7e6bcf4d65..8ed76421894 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint from frappe.utils import flt,cint, cstr, getdate - +from six import iteritems from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate @@ -267,6 +267,9 @@ class BuyingController(StockController): qty_to_be_received_map = get_qty_to_be_received(purchase_orders) for item in self.get('items'): + if not item.purchase_order: + continue + # reset raw_material cost item.rm_supp_cost = 0 @@ -279,11 +282,17 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) + if not fg_yet_to_be_received: + frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") + .format(item.idx, frappe.bold(item.item_code), + frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), + title=_("Limit Crossed")) + transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order) + rm_item_key = (raw_material.rm_item_code, item.purchase_order) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) @@ -313,7 +322,7 @@ class BuyingController(StockController): if raw_material.batch_nos: batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map) + qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) for batch_data in batches_qty: qty = batch_data['qty'] raw_material.batch_no = batch_data['batch'] @@ -325,6 +334,10 @@ class BuyingController(StockController): rm = self.append('supplied_items', {}) rm.update(raw_material_data) + if not rm.main_item_code: + rm.main_item_code = fg_item_doc.item_code + + rm.reference_name = fg_item_doc.name rm.required_qty = qty rm.consumed_qty = qty @@ -835,7 +848,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s AND IFNULL(sed.t_warehouse, '') != '' - AND sed.subcontracted_item = %s + AND IFNULL(sed.subcontracted_item, '') in ('', %s) GROUP BY sed.item_code, sed.subcontracted_item """ raw_materials = frappe.db.multisql({ @@ -852,39 +865,42 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): return raw_materials def get_backflushed_subcontracted_raw_materials(purchase_orders): - common_query = """ - SELECT - CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key, - SUM(prsi.consumed_qty) AS qty, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi - WHERE - pr.name = pri.parent - AND pr.name = prsi.parent - AND pri.purchase_order IN %s - AND pri.item_code = prsi.main_item_code - AND pr.docstatus = 1 - GROUP BY prsi.rm_item_code, pri.purchase_order - """ + purchase_receipts = frappe.get_all("Purchase Receipt Item", + fields = ["purchase_order", "item_code", "name", "parent"], + filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - backflushed_raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')" - ) - }, (purchase_orders, ), as_dict=1) + distinct_purchase_receipts = {} + for pr in purchase_receipts: + key = (pr.purchase_order, pr.item_code, pr.parent) + distinct_purchase_receipts.setdefault(key, []).append(pr.name) backflushed_raw_materials_map = frappe._dict() - for item in backflushed_raw_materials: - backflushed_raw_materials_map.setdefault(item.item_key, item) + for args, references in iteritems(distinct_purchase_receipts): + purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) + + for data in purchase_receipt_supplied_items: + pr_key = (data.rm_item_code, args[0]) + if pr_key not in backflushed_raw_materials_map: + backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ + "qty": 0.0, + "serial_no": [], + "batch_no": [] + })) + + row = backflushed_raw_materials_map.get(pr_key) + row.qty += data.consumed_qty + + for field in ["serial_no", "batch_no"]: + if data.get(field): + row[field].append(data.get(field)) return backflushed_raw_materials_map +def get_supplied_items(item_code, purchase_receipt, references): + return frappe.get_all("Purchase Receipt Item Supplied", + fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"], + filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) + def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], @@ -966,14 +982,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): SELECT sed.batch_no, SUM(sed.qty) AS qty, - sed.item_code + sed.item_code, + sed.subcontracted_item FROM `tabStock Entry` se,`tabStock Entry Detail` sed WHERE se.name = sed.parent AND se.docstatus=1 AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s - AND sed.subcontracted_item = %s + AND ifnull(sed.subcontracted_item, '') in ('', %s) AND sed.batch_no IS NOT NULL GROUP BY sed.batch_no, @@ -981,8 +998,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): """, (purchase_order, fg_item), as_dict=1) for batch_data in transferred_batches: - transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty + key = ((batch_data.item_code, fg_item) + if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) + transferred_batch_qty_map.setdefault(key, {}) + transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty return transferred_batch_qty_map @@ -1019,9 +1038,12 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item): return backflushed_batch_qty_map -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map): +def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map, po): # Returns available batches to be backflushed based on requirements transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) + if not transferred_batches: + transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) + backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {}) available_batches = [] diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b49198579b8..afc63fc8f51 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -613,9 +613,12 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): if not taxes: return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) else: + valid_from = filters.get('valid_from') + valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from + args = { 'item_code': filters.get('item_code'), - 'posting_date': filters.get('valid_from'), + 'posting_date': valid_from, 'tax_category': filters.get('tax_category') } diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b465a106f0e..da99f1267f6 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -249,7 +249,7 @@ class StatusUpdater(Document): args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2f275bb3c91..8cad82c3e25 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -242,10 +242,11 @@ class StockController(AccountsController): _(self.doctype), self.name, item.get("item_code"))) def delete_auto_created_batches(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.items: if not d.batch_no: continue - serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})] + serial_nos = get_serial_nos(d.serial_no) if serial_nos: frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 638500e1240..c8f42a5921f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -525,7 +525,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against: return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self._set_in_company_currency(self.doc, ['write_off_amount']) @@ -543,7 +543,7 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 - if self.doc.doctype == "Sales Invoice": + if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): self.calculate_write_off_amount() self.calculate_change_amount() change_amount = self.doc.change_amount \ @@ -555,6 +555,9 @@ class calculate_taxes_and_totals(object): self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.precision("outstanding_amount")) + if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): + self.update_paid_amount_for_return(total_amount_to_pay) + def calculate_paid_amount(self): paid_amount = base_paid_amount = 0.0 @@ -625,6 +628,27 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) + def update_paid_amount_for_return(self, total_amount_to_pay): + default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', + {'parent': self.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'type', 'account'], as_dict=1) + + self.doc.payments = [] + + if default_mode_of_payment: + self.doc.append('payments', { + 'mode_of_payment': default_mode_of_payment.mode_of_payment, + 'type': default_mode_of_payment.type, + 'account': default_mode_of_payment.account, + 'amount': total_amount_to_pay + }) + else: + self.doc.is_pos = 0 + self.doc.pos_profile = '' + + self.calculate_paid_amount() + + def get_itemised_tax_breakup_html(doc): if not doc.taxes: return diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 3a5a542ba96..3e27670d05d 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -71,7 +71,7 @@ class ProgramEnrollment(Document): def create_course_enrollments(self): student = frappe.get_doc("Student", self.student) program = frappe.get_doc("Program", self.program) - course_list = [course.course for course in program.get_all_children()] + course_list = [course.course for course in program.courses] for course_name in course_list: student.enroll_in_course(course_name=course_name, program_enrollment=self.name) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 087f1628404..0a208128564 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -149,7 +149,9 @@ var check_and_set_availability = function(frm) { primary_action: function() { frm.set_value('appointment_time', selected_slot); frm.set_value('service_unit', service_unit || ''); - frm.set_value('duration', duration); + if (!frm.doc.duration) { + frm.set_value('duration', duration); + } frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f23b4220849..73056f16ed1 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -82,7 +82,8 @@ def add_attendance(events, start, end, conditions=None): e = { "name": d.name, "doctype": "Attendance", - "date": d.attendance_date, + "start": d.attendance_date, + "end": d.attendance_date, "title": cstr(d.status), "docstatus": d.docstatus } diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js index 104f09d69ff..45664896965 100644 --- a/erpnext/hr/doctype/attendance/attendance_calendar.js +++ b/erpnext/hr/doctype/attendance/attendance_calendar.js @@ -1,12 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.views.calendar["Attendance"] = { - field_map: { - "start": "attendance_date", - "end": "attendance_date", - "id": "name", - "docstatus": 1 - }, options: { header: { left: 'prev,next today', diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_list.js b/erpnext/hr/doctype/expense_claim/expense_claim_list.js index 6195ad414a1..9bafc185628 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_list.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Expense Claim'] = { - add_fields: ["total_claimed_amount", "docstatus"], + add_fields: ["total_claimed_amount", "docstatus", "company"], get_indicator: function(doc) { if(doc.status == "Paid") { return [__("Paid"), "green", "status,=,Paid"]; diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 460be514b59..9cc9c87f7f8 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -1,5 +1,4 @@ { - "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-02-20 11:18:11", @@ -167,6 +166,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "default": "Open", "fieldname": "status", "fieldtype": "Select", @@ -246,9 +246,8 @@ "icon": "fa fa-calendar", "idx": 1, "is_submittable": 1, - "links": [], "max_attachments": 3, - "modified": "2020-03-10 22:40:43.487721", + "modified": "2020-08-13 17:22:44.832397", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", @@ -333,4 +332,4 @@ "sort_order": "DESC", "timeline_field": "employee", "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 1622fb38eec..6c42c4752b7 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -433,6 +433,8 @@ def get_leave_details(employee, date): 'from_date': ('<=', date), 'to_date': ('>=', date), 'leave_type': allocation.leave_type, + 'employee': employee, + 'docstatus': 1 }, 'SUM(total_leaves_allocated)') or 0 remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, @@ -597,7 +599,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 + AND docstatus=1 AND (leaves<0 OR is_expired=1) AND (from_date between %(from_date)s AND %(to_date)s @@ -790,4 +792,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver \ No newline at end of file + return leave_approver diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 2a81b32aa9a..f8aad316ed9 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -290,6 +290,7 @@ class PayrollEntry(Document): "account": default_payroll_payable_account, "credit_in_account_currency": flt(payable_amount, precision), "party_type": '', + "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index c5bc92dcc17..c6e89f63bf0 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -54,7 +54,7 @@ class MaintenanceSchedule(TransactionBase): email_map[d.sales_person] = sp.get_email_id() except frappe.ValidationError: no_email_sp.append(d.sales_person) - + if no_email_sp: frappe.msgprint( frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( @@ -66,17 +66,17 @@ class MaintenanceSchedule(TransactionBase): parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1) for key in scheduled_date: - description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - frappe.get_doc({ + description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) + event = frappe.get_doc({ "doctype": "Event", "owner": email_map.get(d.sales_person, self.owner), "subject": description, "description": description, "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "event_type": "Private", - "ref_type": self.doctype, - "ref_name": self.name - }).insert(ignore_permissions=1) + }) + event.add_participant(self.doctype, self.name) + event.insert(ignore_permissions=1) frappe.db.set(self, 'status', 'Submitted') diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index d8ae17b4c7f..3c307e920fc 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,6 +2,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals +from frappe.utils.data import get_datetime, add_days import frappe import unittest @@ -9,4 +10,39 @@ import unittest # test_records = frappe.get_test_records('Maintenance Schedule') class TestMaintenanceSchedule(unittest.TestCase): - pass + def test_events_should_be_created_and_deleted(self): + ms = make_maintenance_schedule() + ms.generate_schedule() + ms.submit() + + all_events = get_events(ms) + self.assertTrue(len(all_events) > 0) + + ms.cancel() + events_after_cancel = get_events(ms) + self.assertTrue(len(events_after_cancel) == 0) + +def get_events(ms): + return frappe.get_all("Event Participants", filters={ + "reference_doctype": ms.doctype, + "reference_docname": ms.name, + "parenttype": "Event" + }) + +def make_maintenance_schedule(): + ms = frappe.new_doc("Maintenance Schedule") + ms.company = "_Test Company" + ms.customer = "_Test Customer" + ms.transaction_date = get_datetime() + + ms.append("items", { + "item_code": "_Test Item", + "start_date": get_datetime(), + "end_date": add_days(get_datetime(), 32), + "periodicity": "Weekly", + "no_of_visits": 4, + "sales_person": "Sales Team", + }) + ms.insert(ignore_permissions=True) + + return ms diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 68f796ba1bc..14bcbaac6ff 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -539,7 +539,7 @@ class BOM(WebsiteGenerator): 'image' : d.image, 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / flt(d.conversion_factor), + 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), 'include_item_in_manufacturing': d.include_item_in_manufacturing })) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index e6c10ad12b0..742d18c4cda 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -90,6 +90,7 @@ def update_latest_price_in_all_boms(): update_cost() def replace_bom(args): + frappe.db.auto_commit_on_many_writes = 1 args = frappe._dict(args) doc = frappe.get_doc("BOM Update Tool") @@ -97,6 +98,8 @@ def replace_bom(args): doc.new_bom = args.new_bom doc.replace_bom() + frappe.db.auto_commit_on_many_writes = 0 + def update_cost(): frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bbce6f55d81..596206fb63c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on('Job Card', { + setup: function(frm) { + frm.set_query('operation', function() { + return { + query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations', + filters: { + 'work_order': frm.doc.work_order + } + }; + }); + }, + refresh: function(frm) { if(frm.doc.docstatus == 0) { @@ -24,12 +35,60 @@ frappe.ui.form.on('Job Card', { } } + frm.trigger("toggle_operation_number"); + if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + operation: function(frm) { + frm.trigger("toggle_operation_number"); + + if (frm.doc.operation && frm.doc.work_order) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details", + args: { + "work_order":frm.doc.work_order, + "operation":frm.doc.operation + }, + callback: function (r) { + if (r.message) { + if (r.message.length == 1) { + frm.set_value("operation_id", r.message[0].name); + } else { + let args = []; + + r.message.forEach((row) => { + args.push({ "label": row.idx, "value": row.name }); + }); + + let description = __("Operation {0} added multiple times in the work order {1}", + [frm.doc.operation, frm.doc.work_order]); + + frm.set_df_property("operation_row_number", "options", args); + frm.set_df_property("operation_row_number", "description", description); + } + + frm.trigger("toggle_operation_number"); + } + } + }) + } + }, + + operation_row_number(frm) { + if (frm.doc.operation_row_number) { + frm.set_value("operation_id", frm.doc.operation_row_number); + } + }, + + toggle_operation_number(frm) { + frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + }, + prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); if (!frm.doc.job_started) { @@ -39,9 +98,9 @@ frappe.ui.form.on('Job Card', { fieldname: 'employee'}, d => { if (d.employee) { frm.set_value("employee", d.employee); + } else { + frm.events.start_job(frm); } - - frm.events.start_job(frm); }, __("Enter Value"), __("Start")); } else { frm.events.start_job(frm); @@ -86,9 +145,7 @@ frappe.ui.form.on('Job Card', { frm.set_value('current_time' , 0); } - frm.save("Save", () => {}, "", () => { - frm.doc.time_logs.pop(-1); - }); + frm.save(); }, complete_job: function(frm, completed_time, completed_qty) { @@ -120,6 +177,8 @@ frappe.ui.form.on('Job Card', { employee: function(frm) { if (frm.doc.job_started && !frm.doc.current_time) { frm.trigger("reset_timer"); + } else { + frm.events.start_job(frm); } }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7661fffa864..b5d17803ee9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -10,6 +10,7 @@ "bom_no", "workstation", "operation", + "operation_row_number", "column_break_4", "posting_date", "company", @@ -287,10 +288,15 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "operation_row_number", + "fieldtype": "Select", + "label": "Operation Row Number" } ], "is_submittable": 1, - "modified": "2020-03-27 13:36:35.417502", + "modified": "2020-08-24 15:21:21.398267", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -342,7 +348,6 @@ "write": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "title_field": "operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 88b20eb6942..c7443d911ce 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -9,10 +9,13 @@ from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff, get_l from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document +class OperationMismatchError(frappe.ValidationError): pass + class JobCard(Document): def validate(self): self.validate_time_logs() self.set_status() + self.validate_operation_id() def validate_time_logs(self): self.total_completed_qty = 0.0 @@ -107,11 +110,10 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - field = "operation_id" if self.operation_id else "operation" + field = "operation_id" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, - "workstation": self.workstation, field: self.get(field)}) + filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty @@ -124,14 +126,13 @@ class JobCard(Document): FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) + and jc.{0} = %s and jc.docstatus = 1 + """.format(field), (self.work_order, self.get(field)), as_dict=1) wo = frappe.get_doc('Work Order', self.work_order) - work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field): + if data.get("name") == self.get(field): data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None @@ -204,6 +205,37 @@ class JobCard(Document): if update_status: self.db_set('status', self.status) + def validate_operation_id(self): + if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and + frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + work_order = frappe.bold(get_link_to_form("Work Order", self.work_order)) + frappe.throw(_("Operation {0} does not belong to the work order {1}") + .format(frappe.bold(self.operation), work_order), OperationMismatchError) + +@frappe.whitelist() +def get_operation_details(work_order, operation): + if work_order and operation: + return frappe.get_all("Work Order Operation", fields = ["name", "idx"], + filters = { + "parent": work_order, + "operation": operation + } + ) + +@frappe.whitelist() +def get_operations(doctype, txt, searchfield, start, page_len, filters): + if filters.get("work_order"): + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) + + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ca05fea0f6f..353f6d281a8 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -4,6 +4,64 @@ from __future__ import unicode_literals import unittest +import frappe +from frappe.utils import random_string +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError class TestJobCard(unittest.TestCase): - pass + def test_job_card(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_cards = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, fields = ["operation_id", "name"]) + + if job_cards: + job_card = job_cards[0] + frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id) + + doc = frappe.get_doc("Job Card", job_card.name) + doc.operation_id = "Test Data" + self.assertRaises(OperationMismatchError, doc.save) + + def test_job_card_with_different_work_station(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_card = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, + fields = ["operation_id", "workstation", "name", "for_quantity"])[0] + + if job_card: + workstation = frappe.db.get_value("Workstation", + {"name": ("not in", [job_card.workstation])}, "name") + + if not workstation or job_card.workstation == workstation: + workstation = make_workstation(workstation_name=random_string(5)).name + + doc = frappe.get_doc("Job Card", job_card.name) + doc.workstation = workstation + doc.append("time_logs", { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity + }) + doc.submit() + + completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + self.assertEqual(completed_qty, job_card.for_quantity) + + doc.cancel() \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8ff691cc300..02dfabe6f70 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -322,12 +322,13 @@ class ProductionPlan(Document): work_orders = [] bom_data = {} - get_sub_assembly_items(item.get("bom_no"), bom_data) + get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) for key, data in bom_data.items(): data.update({ - 'qty': data.get("stock_qty") * item.get("qty"), + 'qty': data.get("stock_qty"), 'production_plan': self.name, + 'use_multi_level_bom': item.get("use_multi_level_bom"), 'company': self.company, 'fg_warehouse': item.get("fg_warehouse"), 'update_consumed_material_cost_in_project': 0 @@ -724,7 +725,7 @@ def get_item_data(item_code): # "description": item_details.get("description") } -def get_sub_assembly_items(bom_no, bom_data): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty): data = get_children('BOM', parent = bom_no) for d in data: if d.expandable: @@ -741,6 +742,6 @@ def get_sub_assembly_items(bom_no, bom_data): }) bom_item = bom_data.get(key) - bom_item["stock_qty"] += d.stock_qty / d.parent_bom_qty + bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - get_sub_assembly_items(bom_item.get("bom_no"), bom_data) + get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 26f580db339..c67330ad45f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -158,6 +158,46 @@ class TestProductionPlan(unittest.TestCase): self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.customer, '_Test Customer') + def test_production_plan_with_multi_level_bom(self): + #|Item Code | Qty | + #|Test BOM 1 | 1 | + #| Test BOM 2 | 2 | + #| Test BOM 3 | 3 | + + for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]: + create_item(item_code, is_stock_item=1) + + # created bom upto 3 level + if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}): + make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3) + + if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}): + make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3) + + if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}): + make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2) + + item_code = "Test BOM 1" + pln = frappe.new_doc('Production Plan') + pln.company = "_Test Company" + pln.append("po_items", { + "item_code": item_code, + "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), + "planned_qty": 3, + "make_work_order_for_sub_assembly_items": 1 + }) + + pln.submit() + pln.make_work_order() + + #last level sub-assembly work order produce qty + to_produce_qty = frappe.db.get_value("Work Order", + {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty") + + self.assertEqual(to_produce_qty, 18.0) + pln.cancel() + frappe.delete_doc("Production Plan", pln.name) + def create_production_plan(**args): args = frappe._dict(args) @@ -205,7 +245,7 @@ def make_bom(**args): bom.append('items', { 'item_code': item, - 'qty': 1, + 'qty': args.rm_qty or 1.0, 'uom': item_doc.stock_uom, 'stock_uom': item_doc.stock_uom, 'rate': item_doc.valuation_rate or args.rate, @@ -213,4 +253,4 @@ def make_bom(**args): bom.insert(ignore_permissions=True) bom.submit() - return bom \ No newline at end of file + return bom diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 21692608548..8266cf7b779 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase): "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + +def make_workstation(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Workstation", + "workstation_name": args.workstation_name + }) + + doc.insert() + + return doc + except frappe.DuplicateEntryError: + return frappe.get_doc("Workstation", args.workstation_name) \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 51d18e83e99..afb6db35f27 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -676,3 +676,5 @@ erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount erpnext.patches.v12_0.set_multi_uom_in_rfq erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v12_0.update_leave_application_status +erpnext.patches.v12_0.update_payment_entry_status \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_leave_application_status.py b/erpnext/patches/v12_0/update_leave_application_status.py new file mode 100644 index 00000000000..261a3a5c0cb --- /dev/null +++ b/erpnext/patches/v12_0/update_leave_application_status.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + # frappe.reload_doc('HR', 'doctype', 'leave_application') + frappe.db.sql(""" + UPDATE `tabLeave Application` SET + status = 'Cancelled' + WHERE status = 'Approved' and docstatus = 2 + """) \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_payment_entry_status.py b/erpnext/patches/v12_0/update_payment_entry_status.py new file mode 100644 index 00000000000..8d033800af3 --- /dev/null +++ b/erpnext/patches/v12_0/update_payment_entry_status.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("accounts", "doctype", "payment_entry") + + frappe.db.sql(""" UPDATE `tabPayment Entry` set status = 'Cancelled' WHERE docstatus = 2 """) \ No newline at end of file diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 802cc056c6a..b45efa22656 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -508,7 +508,7 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { var d = frm.add_child("items"); var item = r.message[i]; for ( var key in item) { - if ( !is_null(item[key]) ) { + if ( !is_null(item[key]) && key !== "doctype" ) { d[key] = item[key]; } } diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2947e880311..1e99aa3bf0b 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -38,6 +38,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_total_advance(update_paid_amount); } + if (this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_pos && + this.frm.doc.is_return) { + this.update_paid_amount_for_return(); + } + // Sales person's commission if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) { this.calculate_commission(); @@ -653,23 +658,58 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - set_default_payment: function(total_amount_to_pay, update_paid_amount){ + update_paid_amount_for_return: function() { + var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + + if(this.frm.doc.party_account_currency == this.frm.doc.currency) { + var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance + - this.frm.doc.write_off_amount), precision("grand_total")); + } else { + var total_amount_to_pay = flt( + (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), + precision("base_grand_total") + ); + } + + frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'account', 'type'], (value) => { + if (this.frm.is_dirty()) { + frappe.model.clear_table(this.frm.doc, 'payments'); + if (value) { + let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); + row.mode_of_payment = value.mode_of_payment; + row.type = value.type; + row.account = value.account; + row.default = 1; + row.amount = total_amount_to_pay; + } else { + this.frm.set_value('is_pos', 1); + } + this.frm.refresh_fields(); + } + }, 'Sales Invoice'); + + this.calculate_paid_amount(); + }, + + set_default_payment: function(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; - if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)){ - $.each(this.frm.doc['payments'] || [], function(index, data){ + if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { + $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { data.base_amount = flt(total_amount_to_pay, precision("base_amount")); data.amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount")); payment_status = false; - }else if(me.frm.doc.paid_amount){ + } else if(me.frm.doc.paid_amount) { data.amount = 0.0; } }); } }, - calculate_paid_amount: function(){ + calculate_paid_amount: function() { var me = this; var paid_amount = 0.0; var base_paid_amount = 0.0; diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b64a68753b5..e16cb5fe6e5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1789,7 +1789,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_query_for_item_tax_template: function(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); if(!item.item_code) { frappe.throw(__("Please enter Item Code to get item taxes")); @@ -1797,7 +1796,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ let filters = { 'item_code': item.item_code, - 'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date, + 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, } diff --git a/erpnext/public/js/pos/pos_bill_item_new.html b/erpnext/public/js/pos/pos_bill_item_new.html index cb626cefcea..b365845d518 100644 --- a/erpnext/public/js/pos/pos_bill_item_new.html +++ b/erpnext/public/js/pos/pos_bill_item_new.html @@ -1,7 +1,7 @@
{%= qty %}
{%= discount_percentage %}
diff --git a/erpnext/public/js/pos/pos_item.html b/erpnext/public/js/pos/pos_item.html index 52f3cf698ae..5b1bb3c167b 100755 --- a/erpnext/public/js/pos/pos_item.html +++ b/erpnext/public/js/pos/pos_item.html @@ -1,12 +1,12 @@
frm.set_value(r.message), () => { frm.updating_party_details = false; - if(callback) callback(); + if (callback) callback(); frm.refresh(); erpnext.utils.add_item(frm); } @@ -110,9 +114,9 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } erpnext.utils.add_item = function(frm) { - if(frm.is_new()) { + if (frm.is_new()) { var prev_route = frappe.get_prev_route(); - if(prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { + if (prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { // add row var item = frm.add_child('items'); frm.refresh_field('items'); @@ -124,23 +128,23 @@ erpnext.utils.add_item = function(frm) { } erpnext.utils.get_address_display = function(frm, address_field, display_field, is_your_company_address) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(!address_field) { - if(frm.doctype != "Purchase Order" && frm.doc.customer) { + if (!address_field) { + if (frm.doctype != "Purchase Order" && frm.doc.customer) { address_field = "customer_address"; - } else if(frm.doc.supplier) { + } else if (frm.doc.supplier) { address_field = "supplier_address"; } else return; } - if(!display_field) display_field = "address_display"; - if(frm.doc[address_field]) { + if (!display_field) display_field = "address_display"; + if (frm.doc[address_field]) { frappe.call({ method: "frappe.contacts.doctype.address.address.get_address_display", args: {"address_dict": frm.doc[address_field] }, callback: function(r) { - if(r.message) { + if (r.message) { frm.set_value(display_field, r.message) } } @@ -151,15 +155,15 @@ erpnext.utils.get_address_display = function(frm, address_field, display_field, }; erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billing_address_field, shipping_address_field) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -175,8 +179,8 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi "shipping_address": frm.doc[shipping_address_field] }, callback: function(r) { - if(!r.exc){ - if(frm.doc.tax_category != r.message) { + if (!r.exc){ + if (frm.doc.tax_category != r.message) { frm.set_value("tax_category", r.message); } else { erpnext.utils.set_taxes(frm, triggered_from_field); @@ -187,13 +191,17 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi }; erpnext.utils.set_taxes = function(frm, triggered_from_field) { - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) { + return; + } + + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -230,7 +238,7 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { "shipping_address": frm.doc.shipping_address_name }, callback: function(r) { - if(r.message){ + if (r.message){ frm.set_value("taxes_and_charges", r.message) } } @@ -238,14 +246,14 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { }; erpnext.utils.get_contact_details = function(frm) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frm.doc["contact_person"]) { + if (frm.doc["contact_person"]) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", args: {contact: frm.doc.contact_person }, callback: function(r) { - if(r.message) + if (r.message) frm.set_value(r.message); } }) @@ -253,10 +261,10 @@ erpnext.utils.get_contact_details = function(frm) { } erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { - if(!value) { + if (!value) { frm.doc[trigger_on] = ""; refresh_field(trigger_on); - frappe.msgprint(__("Please enter {0} first", [label])); + frappe.throw({message:__("Please enter {0} first", [label]), title:__("Mandatory")}); return false; } return true; @@ -271,12 +279,12 @@ erpnext.utils.get_shipping_address = function(frm, callback){ address: frm.doc.shipping_address }, callback: function(r){ - if(r.message){ + if (r.message){ frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } - if(callback){ + if (callback){ return callback(); } } diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js index dfdf9dc0958..b757d53aa23 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js @@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = { ], onload: (report) => { fetch_gstins(report); + + report.page.add_inner_button(__("Download JSON"), function () { + var filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file', + data: r.message.data, + report_name: r.message.report_name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }); } }; diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index a3ed4cebb12..4060a553bd8 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -41,6 +43,12 @@ def _execute(filters=None): data.append(row) added_item.append((d.parent, d.item_code)) + # gst is already added, just add qty and taxable value + else: + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.base_net_amount, d.base_net_amount] + for tax in tax_columns: + row += [0] + data.append(row) if data: data = get_merged_data(columns, data) # merge same hsn code data return columns, data @@ -141,7 +149,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +161,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +183,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +220,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + '''download json content in a file''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d12210..ceea8460bea 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -19,6 +19,11 @@ def execute(filters=None): if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('company', frappe.db.get_default("company")) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) + if region != 'United States': + return [],[] + data = [] columns = get_columns() data = frappe.db.sql(""" diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c8..ad95010a9ac 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase): def test_irs_1099_report(self): make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"}) + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) columns, data = execute_1099_report(filters) print(columns, data) expected_row = {'supplier': '_US 1099 Test Supplier', @@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier(): pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" - pe.company = "_Test Company" + pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC" - pe.paid_to = "_Test Payable USD - _TC" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" pe.paid_amount = 100 pe.received_amount = 100 pe.reference_no = "For IRS 1099 testing" diff --git a/erpnext/selling/doctype/lead_source/lead_source.json b/erpnext/selling/doctype/lead_source/lead_source.json index 868f6d11d04..373e83af9cf 100644 --- a/erpnext/selling/doctype/lead_source/lead_source.json +++ b/erpnext/selling/doctype/lead_source/lead_source.json @@ -1,7 +1,7 @@ { "allow_copy": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:source_name", "beta": 0, "creation": "2016-09-16 01:47:47.382372", @@ -74,7 +74,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-09-16 02:03:01.441622", + "modified": "2020-09-16 02:03:01.441622", "modified_by": "Administrator", "module": "Selling", "name": "Lead Source", @@ -128,4 +128,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_seen": 0 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 3c8ba467680..1748ee353f4 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -262,9 +262,17 @@ def _make_customer(source_name, ignore_permissions=False): return customer else: raise - except frappe.MandatoryError: + except frappe.MandatoryError as e: + mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + frappe.local.message_log = [] - frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) + lead_link = frappe.utils.get_link_to_form("Lead", lead_name) + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" + message += "
  • " + "
  • ".join(mandatory_fields) + "
" + message += _("Please create Customer from Lead {0}.").format(lead_link) + + frappe.throw(message, title=_("Mandatory Missing")) else: return customer_name else: diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index f88289871e9..ed3a96446a0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import json import frappe.utils -from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form +from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form, strip_html from frappe import _ from six import string_types from frappe.model.utils import get_fetch_values @@ -994,15 +994,20 @@ def make_raw_material_request(items, company, sales_order, project=None): )) for item in raw_materials: item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) - material_request.append('items', { - 'item_code': item.get('item_code'), - 'qty': item.get('quantity'), - 'schedule_date': schedule_date, - 'warehouse': item.get('warehouse'), - 'sales_order': sales_order, - 'project': project + row = material_request.append('items', { + 'item_code': item.get('item_code'), + 'qty': item.get('quantity'), + 'schedule_date': schedule_date, + 'warehouse': item.get('warehouse'), + 'sales_order': sales_order, + 'project': project }) + + if not (strip_html(item.get("description")) and strip_html(item_doc.description)): + row.description = item_doc.item_name or item.get('item_code') + material_request.insert() material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5e360902e83..ae0faf2128b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -416,8 +416,44 @@ class TestSalesOrder(unittest.TestCase): # add new item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + test_user.remove_roles("Accounts User") frappe.set_user("Administrator") + def test_update_child_qty_rate_with_workflow(self): + from frappe.model.workflow import apply_workflow + + frappe.set_user("Administrator") + workflow = make_sales_order_workflow() + so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, 'Approve') + + frappe.set_user("Administrator") + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Sales User", "Test Junior Approver") + frappe.set_user(user) + + # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + + frappe.set_user("Administrator") + user2 = 'test2@example.com' + test_user2 = frappe.get_doc('User', user2) + test_user2.add_roles("Sales User", "Test Approver") + frappe.set_user(user2) + + # Test Approver is allowed to edit with grand_total > 200 + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(so.items[0].qty, 2) + + frappe.set_user("Administrator") + test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + workflow.is_active = 0 + workflow.save() + def test_update_child_qty_rate_product_bundle(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): @@ -953,3 +989,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): "reserved_qty")) test_dependencies = ["Currency Exchange"] + +def make_sales_order_workflow(): + if frappe.db.exists('Workflow', 'SO Test Workflow'): + doc = frappe.get_doc("Workflow", "SO Test Workflow") + doc.set("is_active", 1) + doc.save() + return doc + + frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) + frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) + frappe.db.commit() + frappe.cache().hdel('roles', frappe.session.user) + + workflow = frappe.get_doc({ + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + }) + workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) + workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, + condition = 'doc.grand_total < 200' + )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, + condition = 'doc.grand_total > 200' + )) + workflow.insert(ignore_permissions=True) + + return workflow \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 0b93324b19d..30790f0a987 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -286,7 +286,7 @@ erpnext.pos.PointOfSale = class PointOfSale { if (in_list(['serial_no', 'batch_no'], field)) { args[field] = value; } - + // add to cur_frm const item = this.frm.add_child('items', args); frappe.flags.hide_serial_batch_dialog = true; @@ -436,7 +436,7 @@ erpnext.pos.PointOfSale = class PointOfSale { set_primary_action_in_modal() { if (!this.frm.msgbox) { this.frm.msgbox = frappe.msgprint( - `
+ ` ${__('Print')} ${__('New')}` @@ -445,7 +445,15 @@ erpnext.pos.PointOfSale = class PointOfSale { $(this.frm.msgbox.body).find('.btn-default').on('click', () => { this.frm.msgbox.hide(); this.make_new_invoice(); - }) + }); + + $(this.frm.msgbox.body).find('.btn-primary').on('click', () => { + this.frm.msgbox.hide(); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); + }); } } @@ -680,7 +688,10 @@ erpnext.pos.PointOfSale = class PointOfSale { if(this.frm.doc.docstatus != 1 ){ await this.frm.save(); } - this.frm.print_preview.printit(true); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); }); } if(this.frm.doc.items.length == 0){ diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 32445a618d1..0cc0fd487d3 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -241,3 +241,18 @@ class TestBatch(unittest.TestCase): batch.insert() return batch + +def make_new_batch(**args): + args = frappe._dict(args) + + try: + batch = frappe.get_doc({ + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + }).insert() + + except frappe.DuplicateEntryError: + batch = frappe.get_doc("Batch", args.batch_id) + + return batch \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 62aebbaf504..6be20a2e71d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -121,12 +121,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Sales Order'), function() { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index f045e4f9114..61d90392364 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -13,6 +13,7 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry import unittest from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError class TestItemAlternative(unittest.TestCase): def setUp(self): @@ -110,8 +111,11 @@ def make_items(): if not frappe.db.exists('Item', item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + try: + create_stock_reconciliation(item_code="Test FG A RW 1", + warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + except EmptyStockReconciliationItemsError: + pass if frappe.db.exists('Item', 'Test FG A RW 1'): doc = frappe.get_doc('Item', 'Test FG A RW 1') diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 89ca1bef856..27946586eaa 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -101,12 +101,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if (this.frm.doc.docstatus == 0) { this.frm.add_custom_button(__('Purchase Order'), function () { + if (!me.frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", source_doctype: "Purchase Order", target: me.frm, setters: { - supplier: me.frm.doc.supplier || undefined, + supplier: me.frm.doc.supplier, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a150e097d42..be2453373e4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -223,6 +223,15 @@ class PurchaseReceipt(BuyingController): if not stock_value_diff: continue + + # If PR is sub-contracted and fg item rate is zero + # in that case if account for shource and target warehouse are same, + # then GL entries should not be posted + if flt(stock_value_diff) == flt(d.rm_supp_cost) \ + and warehouse_account.get(self.supplier_warehouse) \ + and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]: + continue + gl_entries.append(self.get_gl_dict({ "account": warehouse_account[d.warehouse]["account"], "against": stock_rbnb, @@ -232,16 +241,17 @@ class PurchaseReceipt(BuyingController): }, warehouse_account[d.warehouse]["account_currency"], item=d)) # stock received but not billed - stock_rbnb_currency = get_account_currency(stock_rbnb) - gl_entries.append(self.get_gl_dict({ - "account": stock_rbnb, - "against": warehouse_account[d.warehouse]["account"], - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(d.base_net_amount, d.precision("base_net_amount")), - "credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \ - if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount")) - }, stock_rbnb_currency, item=d)) + if d.base_net_amount: + stock_rbnb_currency = get_account_currency(stock_rbnb) + gl_entries.append(self.get_gl_dict({ + "account": stock_rbnb, + "against": warehouse_account[d.warehouse]["account"], + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(d.base_net_amount, d.precision("base_net_amount")), + "credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \ + if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount")) + }, stock_rbnb_currency, item=d)) negative_expense_to_be_booked += flt(d.item_tax_amount) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 74019c7b3cb..c9cda37c40b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +import json import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, cstr, today, random_string @@ -121,6 +122,87 @@ class TestPurchaseReceipt(unittest.TestCase): rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + def test_subcontracting_gle_fg_item_rate_zero(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + set_perpetual_inventory() + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", + company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + + self.assertFalse(gl_entries) + + set_perpetual_inventory(0) + + def test_subcontracting_over_receipt(self): + """ + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. + """ + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + make_subcontracted_item, create_purchase_order) + from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, + make_rm_stock_entry as make_subcontract_transfer_entry) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code=item_code) + + po = create_purchase_order(item_code=item_code, qty=1, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + #stock raw materials in a warehouse before transfer + make_stock_entry(target="_Test Warehouse - _TC", + item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "_Test Item", qty=1, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": po.supplied_items[0].rm_item_code, + "item_name": "_Test Item", + "qty": po.supplied_items[0].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[1].rm_item_code, + "item_name": "Test Extra Item 1", + "qty": po.supplied_items[1].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[2].rm_item_code, + "item_name": "_Test Item Home Desktop 100", + "qty": po.supplied_items[2].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + } + ] + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.save() + se.submit() + + pr1 = make_purchase_receipt(po.name) + pr2 = make_purchase_receipt(po.name) + + pr1.submit() + self.assertRaises(frappe.ValidationError, pr2.submit) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), @@ -497,6 +579,67 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(pi2.items[0].qty, 2) self.assertEquals(pi2.items[1].qty, 1) + def test_subcontracted_pr_for_multi_transfer_batches(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry, make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + create_purchase_order) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 3" + + make_item('Sub Contracted Raw Material 3', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': 1, + 'create_new_batch': 1 + }) + + create_subcontracted_item(item_code=item_code, has_batch_no=1, + raw_materials=["Sub Contracted Raw Material 3"]) + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + ste1=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) + ste2=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + + transferred_batch = { + ste1.items[0].batch_no : 300, + ste2.items[0].batch_no : 200 + } + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"} + ] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + self.assertEqual(len(se.items), 2) + se.items[0].batch_no = ste1.items[0].batch_no + se.items[1].batch_no = ste2.items[0].batch_no + se.submit() + + supplied_qty = frappe.db.get_value("Purchase Order Item Supplied", + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty") + + self.assertEqual(supplied_qty, 500.00) + + pr = make_purchase_receipt(po.name) + pr.save() + self.assertEqual(len(pr.supplied_items), 2) + + for row in pr.supplied_items: + self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) + + update_backflush_based_on("BOM") + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit, cost_center from `tabGL Entry` where voucher_type=%s and voucher_no=%s @@ -607,7 +750,7 @@ def make_purchase_receipt(**args): "received_qty": received_qty, "rejected_qty": rejected_qty, "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate or 50, + "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", @@ -632,6 +775,33 @@ def make_purchase_receipt(**args): pr.submit() return pr +def create_subcontracted_item(**args): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 + }) + + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) + + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] test_records = frappe.get_test_records('Purchase Receipt') diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index a9f3cd09ef5..b99e98b65c1 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -72,7 +72,8 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry" + "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry", + "reqd": 1 }, { "fieldname": "reference_name", @@ -83,7 +84,8 @@ "label": "Reference Name", "oldfieldname": "purchase_receipt_no", "oldfieldtype": "Link", - "options": "reference_type" + "options": "reference_type", + "reqd": 1 }, { "fieldname": "section_break_7", @@ -230,8 +232,10 @@ ], "icon": "fa fa-search", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-07-12 12:07:23.153698", + "links": [], + "modified": "2020-09-12 16:11:31.910508", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bbdac992b58..f8885a91edc 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -420,6 +420,9 @@ def get_item_details(item_code): from tabItem where name=%s""", item_code, as_dict=True)[0] def get_serial_nos(serial_no): + if isinstance(serial_no, list): + return serial_no + return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') if s.strip()] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index daa71bc6b2e..86e4b3f8b3d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -500,7 +500,7 @@ class StockEntry(StockController): d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * d.qty + d.basic_amount = d.basic_rate * flt(d.qty) def distribute_additional_costs(self): if self.purpose == "Material Issue": @@ -556,8 +556,9 @@ class StockEntry(StockController): qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if (self.purpose == "Send to Subcontractor" and self.purchase_order and - backflush_raw_materials_based_on == 'BOM'): + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + + if (backflush_raw_materials_based_on == 'BOM'): purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code @@ -594,6 +595,11 @@ class StockEntry(StockController): if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": + for row in self.items: + if not row.subcontracted_item: + frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") + .format(row.idx, frappe.bold(row.item_code))) def validate_bom(self): for d in self.get('items'): @@ -797,6 +803,13 @@ class StockEntry(StockController): ret.get('has_batch_no') and not args.get('batch_no')): args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): + subcontract_items = frappe.get_all("Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + + if subcontract_items and len(subcontract_items) == 1: + ret["subcontracted_item"] = subcontract_items[0].main_item_code + return ret def set_items_for_stock_in(self): @@ -1237,9 +1250,15 @@ class StockEntry(StockController): #Update Supplied Qty in PO Supplied Items frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos - SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed - WHERE pos.name = sed.po_detail and sed.docstatus = 1) - WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order) + SET + pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) + FROM + `tabStock Entry Detail` sed, `tabStock Entry` se + WHERE + (pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code) + AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s + ), 0) + WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) #Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 2afabe1480d..84f535912d4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from six import iteritems def get_sle(**args): @@ -483,6 +484,100 @@ class TestStockEntry(unittest.TestCase): serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) + def test_serial_batch_item_stock_entry(self): + """ + Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item + 2) Cancel same Stock Entry + Expected Result: 1) Batch is created with Reference in Serial No + 2) Batch is deleted and Serial No is Inactive + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se.items[0].batch_no + serial_no = get_serial_nos(se.items[0].serial_no)[0] + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, batch_no) + + self.assertEqual(batch_qty, 1) + + se.cancel() + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, None) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + def test_serial_batch_item_qty_deduction(self): + """ + Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch + Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a LinkExistsError + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se1.items[0].batch_no + serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] + + # Check Source (Origin) Document of Batch + self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) + + se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, + batch_no=batch_no) + serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] + + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document of Batch + self.assertRaises(frappe.LinkExistsError, se1.cancel) + frappe.db.rollback() + + se2.cancel() + + # Check decrease in Batch Qty + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Entry 1 is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") + + # Check if Serial No from Stock Entry 2 is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") + def test_warehouse_company_validation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') set_perpetual_inventory(0, company) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 9992d10febe..9d397df8bcd 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -17,6 +16,7 @@ "item_group", "col_break2", "item_name", + "subcontracted_item", "section_break_8", "description", "column_break_10", @@ -57,7 +57,6 @@ "material_request", "material_request_item", "original_item", - "subcontracted_item", "reference_section", "against_stock_entry", "ste_detail", @@ -415,6 +414,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.purpose == 'Send to Subcontractor'", "fieldname": "subcontracted_item", "fieldtype": "Link", "label": "Subcontracted Item", @@ -497,15 +497,12 @@ "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", "fieldname": "set_basic_rate_manually", "fieldtype": "Check", - "label": "Set Basic Rate Manually", - "show_days": 1, - "show_seconds": 1 + "label": "Set Basic Rate Manually" } ], "idx": 1, "istable": 1, - "links": [], - "modified": "2020-06-08 12:57:03.172887", + "modified": "2020-09-04 12:12:35.668198", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0dc87767dde..170a62c8846 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -45,6 +45,7 @@ class StockReconciliation(StockController): def on_cancel(self): self.delete_and_repost_sle() self.make_gl_entries_on_cancel() + self.delete_auto_created_batches() def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" @@ -183,17 +184,11 @@ class StockReconciliation(StockController): from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] - has_serial_no = False - has_batch_no = False - for row in self.items: - item = frappe.get_doc("Item", row.item_code) - if item.has_batch_no: - has_batch_no = True - if item.has_serial_no or item.has_batch_no: - has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) - else: + serialized_items = False + for row in self.items: + item = frappe.get_cached_doc("Item", row.item_code) + if not (item.has_serial_no or item.has_batch_no): if row.serial_no or row.batch_no: frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ .format(row.idx, frappe.bold(row.item_code))) @@ -221,88 +216,91 @@ class StockReconciliation(StockController): sl_entries.append(self.get_sle_for_items(row)) + else: + serialized_items = True + + if serialized_items: + self.get_sle_for_serialized_items(sl_entries) + if sl_entries: - if has_serial_no: - sl_entries = self.merge_similar_item_serial_nos(sl_entries) - - allow_negative_stock = False - if has_batch_no: - allow_negative_stock = True - + allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - if has_serial_no and sl_entries: - self.update_valuation_rate_for_serial_no() + def get_sle_for_serialized_items(self, sl_entries): + self.issue_existing_serial_and_batch(sl_entries) + self.add_new_serial_and_batch(sl_entries) + self.update_valuation_rate_for_serial_no() - def get_sle_for_serialized_items(self, row, sl_entries): + if sl_entries: + sl_entries = self.merge_similar_item_serial_nos(sl_entries) + + def issue_existing_serial_and_batch(self, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle - serial_nos = get_serial_nos(row.serial_no) + for row in self.items: + serial_nos = get_serial_nos(row.serial_no) or [] - - # To issue existing serial nos - if row.current_qty and (row.current_serial_no or row.batch_no): - args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': -1 * row.current_qty, - 'serial_no': row.current_serial_no, - 'batch_no': row.batch_no, - 'valuation_rate': row.current_valuation_rate - }) - - if row.current_serial_no: + # To issue existing serial nos + if row.current_qty and (row.current_serial_no or row.batch_no): + args = self.get_sle_for_items(row) args.update({ - 'qty_after_transaction': 0, + 'actual_qty': -1 * row.current_qty, + 'serial_no': row.current_serial_no, + 'batch_no': row.batch_no, + 'valuation_rate': row.current_valuation_rate }) - sl_entries.append(args) + if row.current_serial_no: + args.update({ + 'qty_after_transaction': 0, + }) - qty_after_transaction = 0 - for serial_no in serial_nos: - args = self.get_sle_for_items(row, [serial_no]) + sl_entries.append(args) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no - }) + qty_after_transaction = 0 + for serial_no in serial_nos: + args = self.get_sle_for_items(row, [serial_no]) - if previous_sle and row.warehouse != previous_sle.get("warehouse"): - # If serial no exists in different warehouse - - warehouse = previous_sle.get("warehouse", '') or row.warehouse - - if not qty_after_transaction: - qty_after_transaction = get_stock_balance(row.item_code, - warehouse, self.posting_date, self.posting_time) - - qty_after_transaction -= 1 - - new_args = args.copy() - new_args.update({ - 'actual_qty': -1, - 'qty_after_transaction': qty_after_transaction, - 'warehouse': warehouse, - 'valuation_rate': previous_sle.get("valuation_rate") + previous_sle = get_previous_sle({ + "item_code": row.item_code, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": serial_no }) - sl_entries.append(new_args) + if previous_sle and row.warehouse != previous_sle.get("warehouse"): + # If serial no exists in different warehouse - if row.qty: - args = self.get_sle_for_items(row) + warehouse = previous_sle.get("warehouse", '') or row.warehouse - args.update({ - 'actual_qty': row.qty, - 'incoming_rate': row.valuation_rate, - 'valuation_rate': row.valuation_rate - }) + if not qty_after_transaction: + qty_after_transaction = get_stock_balance(row.item_code, + warehouse, self.posting_date, self.posting_time) - sl_entries.append(args) + qty_after_transaction -= 1 - if serial_nos == get_serial_nos(row.current_serial_no): - # update valuation rate - self.update_valuation_rate_for_serial_nos(row, serial_nos) + new_args = args.copy() + new_args.update({ + 'actual_qty': -1, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, + 'valuation_rate': previous_sle.get("valuation_rate") + }) + + sl_entries.append(new_args) + + def add_new_serial_and_batch(self, sl_entries): + for row in self.items: + if row.qty: + args = self.get_sle_for_items(row) + + args.update({ + 'actual_qty': row.qty, + 'incoming_rate': row.valuation_rate, + 'valuation_rate': row.valuation_rate + }) + + sl_entries.append(args) def update_valuation_rate_for_serial_no(self): for d in self.items: @@ -360,17 +358,9 @@ class StockReconciliation(StockController): where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) sl_entries = [] - - has_serial_no = False - for row in self.items: - if row.serial_no or row.batch_no or row.current_serial_no: - has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) + self.get_sle_for_serialized_items(sl_entries) if sl_entries: - if has_serial_no: - sl_entries = self.merge_similar_item_serial_nos(sl_entries) - sl_entries.reverse() allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 51d027f22ef..a679c9415d0 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -170,7 +170,7 @@ class TestStockReconciliation(unittest.TestCase): warehouse = "_Test Warehouse for Stock Reco2 - _TC" sr = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + warehouse = warehouse, qty=5, rate=200, do_not_save=1, do_not_submit=1) sr.save(ignore_permissions=True) sr.submit() @@ -204,6 +204,162 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + def test_stock_reco_for_serial_and_batch_item(self): + set_perpetual_inventory() + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + sr = create_stock_reconciliation(item_code=item.item_code, + warehouse = warehouse, qty=1, rate=100) + + batch_no = sr.items[0].batch_no + + serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos), 1) + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no) + + sr.cancel() + + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + if frappe.db.exists("Serial No", serial_nos[0]): + frappe.delete_doc("Serial No", serial_nos[0]) + + def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): + """ + Behaviour: 1) Create Stock Reconciliation, which will be the origin document + of a new batch having a serial no + 2) Create a Stock Entry that adds a serial no to the same batch following this + Stock Reconciliation + 3) Cancel Stock Reconciliation + 4) Cancel Stock Entry + Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since + Stock Entry is dependent on the batch involved + 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases + """ + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.doctype.batch.batch import get_batch_qty + + set_perpetual_inventory() + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + stock_reco = create_stock_reconciliation(item_code=item.item_code, + warehouse = warehouse, qty=1, rate=100) + batch_no = stock_reco.items[0].batch_no + serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] + + stock_entry = make_stock_entry(item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, + batch_no=batch_no) + serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0] + + # Check Batch qty after 2 transactions + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document of Batch + self.assertRaises(frappe.LinkExistsError, stock_reco.cancel) + frappe.db.rollback() + + stock_entry.cancel() + + # Check Batch qty after cancellation + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Reconcilation is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + # Check if Serial No from Stock Entry is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") + + stock_reco.load_from_db() + stock_reco.cancel() + + for sn in (serial_no, serial_no_2): + if frappe.db.exists("Serial No", sn): + frappe.delete_doc("Serial No", sn) + + def test_stock_reco_for_same_item_with_multiple_batches(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + set_perpetual_inventory() + + item_code = "Stock-Reco-batch-Item-2" + warehouse = "_Test Warehouse for Stock Reco3 - _TC" + + create_warehouse("_Test Warehouse for Stock Reco3", {"is_group": 0, + "parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"}) + + batch_item_doc = create_item(item_code, is_stock_item=1) + if not batch_item_doc.has_batch_no: + frappe.db.set_value("Item", item_code, { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "Test-C.####" + }) + + # inward entries with different batch and valuation rate + ste1=make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code=item_code, + target=warehouse, qty=6, basic_rate=700) + ste2=make_stock_entry(posting_date="2012-12-16", posting_time="02:00", item_code=item_code, + target=warehouse, qty=3, basic_rate=200) + ste3=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code, + target=warehouse, qty=2, basic_rate=500) + ste4=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code, + target=warehouse, qty=4, basic_rate=100) + + batchwise_item_details = {} + for stock_doc in [ste1, ste2, ste3, ste4]: + self.assertEqual(item_code, stock_doc.items[0].item_code) + batchwise_item_details[stock_doc.items[0].batch_no] = [stock_doc.items[0].qty, 0.01] + + stock_balance = frappe.get_all("Stock Ledger Entry", + filters = {"item_code": item_code, "warehouse": warehouse}, + fields=["sum(stock_value_difference)"], as_list=1) + + self.assertEqual(flt(stock_balance[0][0]), 6200.00) + + sr = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, batch_details = batchwise_item_details) + + stock_balance = frappe.get_all("Stock Ledger Entry", + filters = {"item_code": item_code, "warehouse": warehouse}, + fields=["sum(stock_value_difference)"], as_list=1) + + self.assertEqual(flt(stock_balance[0][0]), 0.15) + + for doc in [sr, ste1, ste2, ste3, ste4]: + doc.cancel() + frappe.delete_doc(doc.doctype, doc.name) def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -251,20 +407,33 @@ def create_stock_reconciliation(**args): or frappe.get_cached_value("Company", sr.company, "cost_center") \ or "_Test Cost Center - _TC" - sr.append("items", { - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty, - "valuation_rate": args.rate, - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + if not args.batch_details: + sr.append("items", { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty, + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no + }) + elif args.batch_details: + for batch, data in args.batch_details.items(): + sr.append("items", { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": data[0], + "valuation_rate": data[1], + "batch_no": batch + }) + + if not args.do_not_save: + sr.insert() + try: + if not args.do_not_submit: + sr.submit() + except EmptyStockReconciliationItemsError: + pass - try: - if not args.do_not_submit: - sr.submit() - except EmptyStockReconciliationItemsError: - pass return sr def set_valuation_method(item_code, valuation_method): diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js index 52ddae2624c..e7db3a368df 100644 --- a/erpnext/templates/generators/item/item_inquiry.js +++ b/erpnext/templates/generators/item/item_inquiry.js @@ -20,6 +20,13 @@ frappe.ready(() => { options: 'Email', reqd: 1 }, + { + fieldtype: 'Data', + label: __('Phone Number'), + fieldname: 'phone', + options: 'Phone', + reqd: 1 + }, { fieldtype: 'Data', label: __('Subject'),