diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index b603ed5e53..9cc4663c39 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -21,8 +21,8 @@ def docs_link_exists(body): if word.startswith('http') and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - _, org, repo, _type, ref = parsed_url.path.split('/') - if org == "frappe" and repo in docs_repos: + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py index 6c83e3bd67..acb11e557a 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py @@ -245,6 +245,9 @@ def get(): "account_number": "2200" }, _("Duties and Taxes"): { + _("TDS Payable"): { + "account_number": "2310" + }, "account_type": "Tax", "is_group": 1, "account_number": "2300" diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 27546335c9..e9fc5f0a1d 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -9,11 +9,13 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile test_dependencies = ["Item", "Cost Center"] class TestBankTransaction(unittest.TestCase): def setUp(self): + make_pos_profile() add_transactions() add_payments() @@ -27,6 +29,9 @@ class TestBankTransaction(unittest.TestCase): frappe.db.sql("""delete from `tabPayment Entry Reference`""") frappe.db.sql("""delete from `tabPayment Entry`""") + # Delete POS Profile + frappe.db.sql("delete from `tabPOS Profile`") + frappe.flags.test_bank_transactions_created = False frappe.flags.test_payments_created = False diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index d51856a8a4..ee2092adcc 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -155,7 +155,8 @@ class OpeningInvoiceCreationTool(Document): "posting_date": row.posting_date, frappe.scrub(row.party_type): row.party, "is_pos": 0, - "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" + "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", + "update_stock": 0 }) accounting_dimension = get_accounting_dimensions() diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 54229f5247..bdfe532b9f 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -7,17 +7,24 @@ import frappe import unittest test_dependencies = ["Customer", "Supplier"] +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account class TestOpeningInvoiceCreationTool(unittest.TestCase): - def make_invoices(self, invoice_type="Sales"): + def setUp(self): + if not frappe.db.exists("Company", "_Test Opening Invoice Company"): + make_company() + + def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None): doc = frappe.get_single("Opening Invoice Creation Tool") - args = get_opening_invoice_creation_dict(invoice_type=invoice_type) + args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, + party_1=party_1, party_2=party_2) doc.update(args) return doc.make_invoices() def test_opening_sales_invoice_creation(self): - invoices = self.make_invoices() + property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") + invoices = self.make_invoices(company="_Test Opening Invoice Company") self.assertEqual(len(invoices), 2) expected_value = { @@ -27,6 +34,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): } self.check_expected_values(invoices, expected_value) + si = frappe.get_doc("Sales Invoice", invoices[0]) + + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) + + property_setter.delete() + def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" @@ -36,7 +50,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx]) def test_opening_purchase_invoice_creation(self): - invoices = self.make_invoices(invoice_type="Purchase") + invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company") self.assertEqual(len(invoices), 2) expected_value = { @@ -46,6 +60,32 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): } self.check_expected_values(invoices, expected_value, "Purchase") + def test_opening_sales_invoice_creation_with_missing_debit_account(self): + company = "_Test Opening Invoice Company" + party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") + + old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") + frappe.db.set_value("Company", company, "default_receivable_account", "") + + if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): + cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company", + "is_group": 1, "company": "_Test Opening Invoice Company"}) + cc.insert(ignore_mandatory=True) + cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0, + "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name}) + cc2.insert() + + frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") + + self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) + + # Check if missing debit account error raised + error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) + self.assertTrue(error_log) + + # teardown + frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") @@ -57,7 +97,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 1.0, "outstanding_amount": 300, - "party": "_Test {0}".format(party), + "party": args.get("party_1") or "_Test {0}".format(party), "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", @@ -66,7 +106,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 2.0, "outstanding_amount": 250, - "party": "_Test {0} 1".format(party), + "party": args.get("party_2") or "_Test {0} 1".format(party), "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", @@ -76,4 +116,31 @@ def get_opening_invoice_creation_dict(**args): }) invoice_dict.update(args) - return invoice_dict \ No newline at end of file + return invoice_dict + +def make_company(): + if frappe.db.exists("Company", "_Test Opening Invoice Company"): + return frappe.get_doc("Company", "_Test Opening Invoice Company") + + company = frappe.new_doc("Company") + company.company_name = "_Test Opening Invoice Company" + company.abbr = "_TOIC" + company.default_currency = "INR" + company.country = "India" + company.insert() + return company + +def make_customer(customer=None): + customer_name = customer or "Opening Customer" + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": customer_name, + "customer_group": "All Customer Groups", + "customer_type": "Company", + "territory": "All Territories" + }) + if not frappe.db.exists("Customer", customer_name): + customer.insert(ignore_permissions=True) + return customer.name + else: + return frappe.db.exists("Customer", customer_name) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 570111acac..d856ae3476 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -14,7 +14,6 @@ "column_break_9", "update_stock", "ignore_pricing_rule", - "hide_unavailable_items", "warehouse", "campaign", "company_address", @@ -23,6 +22,9 @@ "section_break_11", "payments", "section_break_14", + "hide_images", + "hide_unavailable_items", + "auto_add_item_to_cart", "item_groups", "column_break_16", "customer_groups", @@ -124,7 +126,8 @@ }, { "fieldname": "section_break_14", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Configuration" }, { "description": "Only show Items from these Item Groups", @@ -314,13 +317,25 @@ "fieldname": "hide_unavailable_items", "fieldtype": "Check", "label": "Hide Unavailable Items" + }, + { + "default": "0", + "fieldname": "hide_images", + "fieldtype": "Check", + "label": "Hide Images" + }, + { + "default": "0", + "fieldname": "auto_add_item_to_cart", + "fieldtype": "Check", + "label": "Automatically Add Filtered Item To Cart" } ], "icon": "icon-cog", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-29 13:18:38.795925", + "modified": "2020-12-10 13:59:28.877572", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index edf86590c8..62dc1fcb20 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -70,6 +70,7 @@ def get_items_list(pos_profile, company): """.format(cond=cond), tuple([company] + args_list), as_dict=1) def make_pos_profile(**args): + frappe.db.sql("delete from `tabPOS Payment Method`") frappe.db.sql("delete from `tabPOS Profile`") args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index cc8ed4bc49..d08a854142 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -406,6 +406,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", @@ -469,6 +470,7 @@ "options": "UOM" }, { + "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", "label": "Rate" @@ -563,7 +565,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2020-10-28 16:53:14.416172", + "modified": "2020-12-04 00:36:24.698219", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ec0a485bfc..af8d21d9ce 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -521,6 +521,22 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_pricing_rule_for_transaction(self): + make_item("Water Flask 1") + frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product", + apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10) + + si = create_sales_invoice(qty=5, do_not_submit=True) + self.assertEquals(len(si.items), 2) + self.assertEquals(si.items[1].rate, 10) + + si1 = create_sales_invoice(qty=2, do_not_submit=True) + self.assertEquals(len(si1.items), 1) + + for doc in [si, si1]: + doc.delete() + def make_pricing_rule(**args): args = frappe._dict(args) @@ -539,20 +555,23 @@ def make_pricing_rule(**args): "rate_or_discount": args.rate_or_discount or "Discount Percentage", "discount_percentage": args.discount_percentage or 0.0, "rate": args.rate or 0.0, - "margin_type": args.margin_type, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "condition": args.condition or '', "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 }) - if args.get("priority"): - doc.priority = args.get("priority") + for field in ["free_item", "free_qty", "free_item_rate", "priority", + "margin_type", "price_or_product_discount"]: + if args.get(field): + doc.set(field, args.get(field)) apply_on = doc.apply_on.replace(' ', '_').lower() child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} - doc.append(child_table.get(doc.apply_on), { - apply_on: args.get(apply_on) or "_Test Item" - }) + + if doc.apply_on != "Transaction": + doc.append(child_table.get(doc.apply_on), { + apply_on: args.get(apply_on) or "_Test Item" + }) doc.insert(ignore_permissions=True) if args.get(apply_on) and apply_on != "item_code": diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b003328cc4..2c7cd14451 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -457,6 +457,9 @@ def apply_pricing_rule_on_transaction(doc): pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules) + if not pricing_rules: + remove_free_item(doc) + for d in pricing_rules: if d.price_or_product_discount == 'Price': if d.apply_discount_on: @@ -480,6 +483,12 @@ def apply_pricing_rule_on_transaction(doc): get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() + doc.calculate_taxes_and_totals() + +def remove_free_item(doc): + for d in doc.items: + if d.is_free_item: + doc.remove(d) def get_applied_pricing_rules(pricing_rules): if pricing_rules: @@ -492,7 +501,7 @@ def get_applied_pricing_rules(pricing_rules): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item - if pricing_rule.same_item: + if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction': free_item = item_details.item_code or args.item_code if not free_item: diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 31356c6e8b..e08a0e5cc2 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -21,7 +21,7 @@ class TestProcessDeferredAccounting(unittest.TestCase): item.no_of_months = 12 item.save() - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True) + si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-10" si.items[0].service_end_date = "2019-03-15" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3fa6ee76e6..1fb3cfb0d2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ return (doc.qty<=doc.received_qty) ? "green" : "orange"; }); } + + this.frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); }, company: function() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2df77a84c7..c64ffd878c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -127,6 +126,7 @@ "write_off_cost_center", "advances_section", "allocate_advances_automatically", + "adjust_advance_taxes", "get_advances", "advances", "payment_schedule_section", @@ -152,9 +152,11 @@ "is_opening", "against_expense_account", "column_break_63", + "unrealized_profit_loss_account", "status", "inter_company_invoice_reference", "is_internal_supplier", + "represents_company", "remarks", "subscription_section", "from_date", @@ -1223,7 +1225,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, { @@ -1330,13 +1332,37 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "description": "Taxes paid while advance payment will be adjusted against this invoice", + "fieldname": "adjust_advance_taxes", + "fieldtype": "Check", + "label": "Adjust Advance Taxes" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Company which internal supplier represents", + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:18.266978", + "modified": "2020-12-11 12:46:12.796378", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 91c4dfb587..d94d261c6b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -147,6 +147,11 @@ class PurchaseInvoice(BuyingController): throw(_("Conversion rate cannot be 0 or 1")) def validate_credit_to_acc(self): + if not self.credit_to: + self.credit_to = get_party_account("Supplier", self.supplier, self.company) + if not self.credit_to: + self.raise_missing_debit_credit_account_error("Supplier", self.supplier) + account = frappe.db.get_value("Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True) @@ -201,8 +206,8 @@ class PurchaseInvoice(BuyingController): ["Purchase Receipt", "purchase_receipt", "pr_detail"] ]) - def validate_warehouse(self): - if self.update_stock: + def validate_warehouse(self, for_validate=True): + if self.update_stock and for_validate: for d in self.get('items'): if not d.warehouse: frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). @@ -228,7 +233,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock: self.validate_item_code() - self.validate_warehouse() + self.validate_warehouse(for_validate) if auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -444,6 +449,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -452,7 +458,6 @@ class PurchaseInvoice(BuyingController): self.make_payment_gl_entries(gl_entries) self.make_write_off_gl_entry(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries) - return gl_entries def check_asset_cwip_enabled(self): @@ -469,31 +474,30 @@ class PurchaseInvoice(BuyingController): # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: - # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) - ) + if grand_total and not self.is_internal_transfer(): + # Didnot use base_grand_total to book rounding loss gle + grand_total_in_company_currency = flt(grand_total * self.conversion_rate, + self.precision("grand_total")) + gl_entries.append( + self.get_gl_dict({ + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": self.against_expense_account, + "credit": grand_total_in_company_currency, + "credit_in_account_currency": grand_total_in_company_currency \ + if self.party_account_currency==self.company_currency else grand_total, + "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center + }, self.party_account_currency, item=self) + ) def make_item_gl_entries(self, gl_entries): # item gl entries stock_items = self.get_stock_items() - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") if self.update_stock and self.auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -521,7 +525,6 @@ class PurchaseInvoice(BuyingController): item, voucher_wise_stock_value, account_currency) if item.from_warehouse: - gl_entries.append(self.get_gl_dict({ "account": warehouse_account[item.warehouse]['account'], "against": warehouse_account[item.from_warehouse]["account"], @@ -541,16 +544,18 @@ class PurchaseInvoice(BuyingController): "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) - ) + # Do not book expense for transfer within same company transfer + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) else: gl_entries.append( @@ -827,7 +832,8 @@ class PurchaseInvoice(BuyingController): }, account_currency, item=tax) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \ + and not self.is_internal_transfer(): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) valuation_tax.setdefault(tax.name, 0) @@ -871,8 +877,19 @@ class PurchaseInvoice(BuyingController): "against": self.supplier, "credit": valuation_tax[tax.name], "remarks": self.remarks or "Accounting Entry for Stock" - }, item=tax) - ) + }, item=tax)) + + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.supplier, + "credit": flt(self.total_taxes_and_charges), + "credit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries @@ -1032,7 +1049,9 @@ class PurchaseInvoice(BuyingController): updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) for pr in set(updated_pr): - frappe.get_doc("Purchase Receipt", pr).update_billing_percentage(update_modified=update_modified) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage + pr_doc = frappe.get_doc("Purchase Receipt", pr) + update_billing_percentage(pr_doc, update_modified=update_modified) def on_recurring(self, reference_doc, auto_repeat_doc): self.due_date = None @@ -1088,7 +1107,9 @@ class PurchaseInvoice(BuyingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate: + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 86c2e408c0..8da7d6fe13 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -4,23 +4,25 @@ // render frappe.listview_settings['Purchase Invoice'] = { add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company", - "currency", "is_return", "release_date", "on_hold"], + "currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"], get_indicator: function(doc) { - if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { + if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"]; - } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { + } else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { if(cint(doc.on_hold) && !doc.release_date) { return [__("On Hold"), "darkgrey"]; - } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { + } else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { return [__("Temporarily on Hold"), "darkgrey"]; - } else if(frappe.datetime.get_diff(doc.due_date) < 0) { + } else if (frappe.datetime.get_diff(doc.due_date) < 0) { return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"]; } else { return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"]; } - } else if(cint(doc.is_return)) { + } else if (cint(doc.is_return)) { return [__("Return"), "darkgrey", "is_return,=,Yes"]; - } else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) { + } else if (doc.company == doc.represents_company && doc.is_internal_supplier) { + return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"]; + } else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) { return [__("Paid"), "green", "outstanding_amount,=,0"]; } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b89cecfab5..53c29aa3e3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -575,6 +575,25 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("cost_center", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 17fbe2def9..6799fb986a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", @@ -158,6 +157,7 @@ "more_information", "inter_company_invoice_reference", "is_internal_customer", + "represents_company", "customer_group", "campaign", "is_discounted", @@ -171,6 +171,7 @@ "c_form_applicable", "c_form_no", "column_break8", + "unrealized_profit_loss_account", "remarks", "sales_team_section_break", "sales_partner", @@ -1655,7 +1656,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, "read_only": 1 }, @@ -1950,13 +1951,31 @@ "fieldtype": "Data", "label": "Company Tax ID", "read_only": 1 + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Company which internal customer represents", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:45.086303", + "modified": "2020-12-11 12:48:31.769958", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index af6c6968dc..ca6f22cc30 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -405,6 +405,8 @@ class SalesInvoice(SellingController): from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} + if not pos_profile: + frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) self.pos_profile = pos_profile.get('name') pos = {} @@ -472,6 +474,11 @@ class SalesInvoice(SellingController): return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] def validate_debit_to_acc(self): + if not self.debit_to: + self.debit_to = get_party_account("Customer", self.customer, self.company) + if not self.debit_to: + self.raise_missing_debit_credit_account_error("Customer", self.customer) + account = frappe.get_cached_value("Account", self.debit_to, ["account_type", "report_type", "account_currency"], as_dict=True) @@ -751,6 +758,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) @@ -770,7 +778,7 @@ class SalesInvoice(SellingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: + if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) @@ -809,6 +817,18 @@ class SalesInvoice(SellingController): }, account_currency, item=tax) ) + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.customer, + "debit": flt(self.total_taxes_and_charges), + "debit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) + def make_item_gl_entries(self, gl_entries): # income account gl entries for item in self.get("items"): @@ -831,22 +851,24 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", self.posting_date) asset.set_status("Sold" if self.docstatus==1 else None) else: - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + # Do not book income for transfer within same company + if not self.is_internal_transfer(): + income_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) - account_currency = get_account_currency(income_account) - gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(item.base_net_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) - if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + account_currency = get_account_currency(income_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_account, + "against": self.customer, + "credit": flt(item.base_net_amount, item.precision("base_net_amount")), + "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) + if account_currency==self.company_currency + else flt(item.net_amount, item.precision("net_amount"))), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # expense account gl entries if cint(self.update_stock) and \ @@ -1258,7 +1280,9 @@ class SalesInvoice(SellingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': self.status = "Overdue and Discounted" elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" @@ -1523,9 +1547,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + source_document_warehouse_field = 'target_warehouse' + target_document_warehouse_field = 'from_warehouse' else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" + source_document_warehouse_field = 'from_warehouse' + target_document_warehouse_field = 'target_warehouse' validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -1552,6 +1580,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if currency: target_doc.currency = currency + item_field_map = { + "doctype": target_doctype + " Item", + "field_no_map": [ + "income_account", + "expense_account", + "cost_center", + "warehouse" + ] + } + + if source_doc.get('update_stock'): + item_field_map.update({ + 'field_map': { + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + } + }) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, @@ -1560,15 +1608,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "taxes_and_charges" ] }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_no_map": [ - "income_account", - "expense_account", - "cost_center", - "warehouse" - ] - } + doctype +" Item": item_field_map }, target_doc, set_missing_values) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 05d49df711..41140d1938 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = { "Credit Note Issued": "darkgrey", "Unpaid and Discounted": "orange", "Overdue and Discounted": "red", - "Overdue": "red" - + "Overdue": "red", + "Internal Transfer": "darkgrey" }; return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9660c9570e..22a4f33654 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -690,7 +690,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - make_pos_profile() + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") @@ -746,7 +747,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(pos_return.get('payments')[0].amount, -1000) def test_pos_change_amount(self): - make_pos_profile() + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") @@ -1571,7 +1573,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1605,9 +1607,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) @@ -1779,6 +1781,60 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) + + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) + + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() + + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], + ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] + ] + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + def test_eway_bill_json(self): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ @@ -2037,4 +2093,57 @@ def get_taxes_and_charges(): "parentfield": "taxes", "rate": 2, "row_id": 1 - }] \ No newline at end of file + }] + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company + }) + + customer.append("companies", { + "company": allowed_to_interact_with + }) + + customer.insert() + customer_name = customer.name + else: + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name + +def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company + }) + + supplier.append("companies", { + "company": allowed_to_interact_with + }) + + supplier.insert() + supplier_name = supplier.name + else: + supplier_name = frappe.db.exists("Supplier", supplier_name) + + return supplier_name + +def add_taxes(doc): + doc.append('taxes', { + 'account_head': '_Test Account Excise Duty - TCP1', + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12 + }) \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index bb0d0a132a..79a6aabd98 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -42,11 +42,13 @@ {% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); - var range1 = report.columns[11].label; - var range2 = report.columns[12].label; - var range3 = report.columns[13].label; - var range4 = report.columns[14].label; - var range5 = report.columns[15].label; + var start = filters.based_on_payment_terms ? 13 : 11; + var range1 = report.columns[start].label; + var range2 = report.columns[start+1].label; + var range3 = report.columns[start+2].label; + var range4 = report.columns[start+3].label; + var range5 = report.columns[start+4].label; + var range6 = report.columns[start+5].label; %} {% if(balance_row) { %} @@ -70,20 +72,34 @@ + - - - - - + + + + + + + @@ -91,6 +107,7 @@ + @@ -101,6 +118,7 @@ + @@ -218,15 +236,15 @@ + {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %} {% if(!filters.show_future_payments) { %} - + {%= format_currency(data[i]["paid"], data[i]["currency"]) %} + {% } %} + {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %} {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} @@ -234,8 +252,8 @@ {%= data[i]["po_no"] %} {% } %} - - + + {% } %} {% } %} {% } else { %} @@ -256,10 +274,10 @@ {% } else { %} {% } %} - - - - + + + + {% } %} {% } %} diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 3ffb3ac1df..515fd995e6 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -14,11 +14,93 @@ def execute(filters=None): def get_column(): return [ - _("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + { + "label": _("Delivery Note"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Delivery Note", + "width": 160 + }, + { + "label": _("Date"), + "fieldname": "date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 120 + }, + { + "label": _("Customer Name"), + "fieldname": "customer_name", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120 + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency" + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency" + }, + { + "label": _("Returned Amount"), + "fieldname": "returned_amount", + "fieldtype": "Currency", + "width": 120, + "options": "Company:company:default_currency" + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 120, + "options": "Company:company:default_currency" + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 120 + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120 + } ] def get_args(): diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 3445df7206..a36e7f8581 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -8,6 +8,7 @@ from frappe.utils import flt from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (get_tax_accounts, get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row, get_group_by_conditions) +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details def execute(filters=None): return _execute(filters) @@ -22,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum aii_account_map = get_aii_accounts() if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency, - doctype="Purchase Invoice", tax_doctype="Purchase Taxes and Charges") + doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges') po_pr_map = get_purchase_receipts_against_purchase_order(item_list) @@ -34,10 +35,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Purchase Invoice') + item_details = get_item_details() + for d in item_list: if not d.stock_qty: continue + item_record = item_details.get(d.item_code) + purchase_receipt = None if d.purchase_receipt: purchase_receipt = d.purchase_receipt @@ -48,8 +53,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, @@ -81,10 +86,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), + frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), }) - total_tax += flt(item_tax.get("tax_amount")) + total_tax += flt(item_tax.get('tax_amount')) row.update({ 'total_tax': total_tax, @@ -309,8 +314,8 @@ def get_items(filters, additional_query_columns): select `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, - `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice Item`.`item_code`, - `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.description, + `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index a05dcd75ce..f54ceb0d2f 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -8,6 +8,7 @@ from frappe.utils import flt, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details, get_customer_details def execute(filters=None): return _execute(filters) @@ -16,7 +17,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if not filters: filters = {} columns = get_columns(additional_table_columns, filters) - company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') item_list = get_items(filters, additional_query_columns) if item_list: @@ -33,7 +34,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Sales Invoice') + customer_details = get_customer_details() + item_details = get_item_details() + for d in item_list: + customer_record = customer_details.get(d.customer) + item_record = item_details.get(d.item_code) + delivery_note = None if d.delivery_note: delivery_note = d.delivery_note @@ -45,14 +52,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, 'customer': d.customer, - 'customer_name': d.customer_name, - 'customer_group': d.customer_group, + 'customer_name': customer_record.customer_name, + 'customer_group': customer_record.customer_group, } if additional_query_columns: @@ -90,10 +97,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), + frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), }) - total_tax += flt(item_tax.get("tax_amount")) + total_tax += flt(item_tax.get('tax_amount')) row.update({ 'total_tax': total_tax, @@ -226,7 +233,7 @@ def get_columns(additional_table_columns, filters): if filters.get('group_by') != 'Territory': columns.extend([ { - 'label': _("Territory"), + 'label': _('Territory'), 'fieldname': 'territory', 'fieldtype': 'Link', 'options': 'Territory', @@ -374,13 +381,12 @@ def get_items(filters, additional_query_columns): `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, - `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, - `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.description, `tabSales Invoice Item`.sales_order, - `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, - `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, - `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, - `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, - `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, + `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, + `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, + `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, + `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} from `tabSales Invoice`, `tabSales Invoice Item` where `tabSales Invoice`.name = `tabSales Invoice Item`.parent @@ -417,14 +423,14 @@ def get_deducted_taxes(): return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'") def get_tax_accounts(item_list, columns, company_currency, - doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): + doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'): import json item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), + tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'), currency=company_currency) or 2 for d in item_list: @@ -469,8 +475,8 @@ def get_tax_accounts(item_list, columns, company_currency, tax_rate = tax_data tax_amount = 0 - if charge_type == "Actual" and not tax_rate: - tax_rate = "NA" + if charge_type == 'Actual' and not tax_rate: + tax_rate = 'NA' item_net_amount = sum([flt(d.base_net_amount) for d in item_row_map.get(parent, {}).get(item_code, [])]) @@ -484,17 +490,17 @@ def get_tax_accounts(item_list, columns, company_currency, if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": tax_rate, - "tax_amount": tax_value + 'tax_rate': tax_rate, + 'tax_amount': tax_value }) except ValueError: continue - elif charge_type == "Actual" and tax_amount: + elif charge_type == 'Actual' and tax_amount: for d in invoice_item_row.get(parent, []): itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": "NA", - "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, + 'tax_rate': 'NA', + 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision) }) @@ -563,7 +569,7 @@ def add_total_row(data, filters, prev_group_by_value, item, total_row_map, }) total_row_map.setdefault('total_row', { - subtotal_display_field: "Total", + subtotal_display_field: 'Total', 'stock_qty': 0.0, 'amount': 0.0, 'bold': 1, diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index a9e25bc25b..2e18ce11dd 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -17,18 +17,26 @@ def get_ordered_to_be_billed_data(args): return frappe.db.sql(""" Select - `{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, - {project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount, + `{parent_tab}`.name, `{parent_tab}`.{date_field}, + `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, + `{child_tab}`.item_code, + `{child_tab}`.base_amount, (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)), - (`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))), - `{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)), + (`{child_tab}`.base_amount - + (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))), + `{child_tab}`.item_name, `{child_tab}`.description, + {project_field}, `{parent_tab}`.company from `{parent_tab}`, `{child_tab}` where `{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1 and `{parent_tab}`.status not in ('Closed', 'Completed') - and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt * - ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount + and `{child_tab}`.amount > 0 + and (`{child_tab}`.base_amount - + round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 order by `{parent_tab}`.{order} {order_by} """.format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party, diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index 5e8d7730b7..e9e9c9c4e6 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -14,11 +14,93 @@ def execute(filters=None): def get_column(): return [ - _("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + { + "label": _("Purchase Receipt"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Purchase Receipt", + "width": 160 + }, + { + "label": _("Date"), + "fieldname": "date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 120 + }, + { + "label": _("Supplier Name"), + "fieldname": "supplier_name", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120 + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency" + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency" + }, + { + "label": _("Returned Amount"), + "fieldname": "returned_amount", + "fieldtype": "Currency", + "width": 120, + "options": "Company:company:default_currency" + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 120, + "options": "Company:company:default_currency" + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 120 + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120 + } ] def get_args(): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 53677cde8a..550aaef404 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -78,7 +78,10 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb else: return ((fy.name, fy.year_start_date, fy.year_end_date),) - error_msg = _("""{0} {1} not in any active Fiscal Year.""").format(label, formatdate(transaction_date)) + error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) + if company: + error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) + if verbose==1: frappe.msgprint(error_msg) raise FiscalYearError(error_msg) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index fd702c74c7..74ca62ffda 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -13,8 +13,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g class AssetValueAdjustment(Document): def validate(self): self.validate_date() - self.set_difference_amount() self.set_current_asset_value() + self.set_difference_amount() def on_submit(self): self.make_depreciation_entry() @@ -25,7 +25,7 @@ class AssetValueAdjustment(Document): frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) self.reschedule_depreciations(self.current_asset_value) - + def validate_date(self): asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date') if getdate(self.date) < getdate(asset_purchase_date): @@ -53,6 +53,7 @@ class AssetValueAdjustment(Document): je.posting_date = self.date je.company = self.company je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) + je.finance_book = self.finance_book credit_entry = { "account": accumulated_depreciation_account, @@ -78,7 +79,7 @@ class AssetValueAdjustment(Document): debit_entry.update({ dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') }) - + je.append("accounts", credit_entry) je.append("accounts", debit_entry) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index af08a2a601..d1457b9b85 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -75,24 +75,23 @@ def get_data(filters): for asset in assets_record: asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ - flt(depreciation_amount_map.get(asset.name)) - if asset_value: - row = { - "asset_id": asset.asset_id, - "asset_name": asset.asset_name, - "status": asset.status, - "department": asset.department, - "cost_center": asset.cost_center, - "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), - "gross_purchase_amount": asset.gross_purchase_amount, - "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, - "available_for_use_date": asset.available_for_use_date, - "location": asset.location, - "asset_category": asset.asset_category, - "purchase_date": asset.purchase_date, - "asset_value": asset_value - } - data.append(row) + row = { + "asset_id": asset.asset_id, + "asset_name": asset.asset_name, + "status": asset.status, + "department": asset.department, + "cost_center": asset.cost_center, + "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), + "gross_purchase_amount": asset.gross_purchase_amount, + "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, + "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "available_for_use_date": asset.available_for_use_date, + "location": asset.location, + "asset_category": asset.asset_category, + "purchase_date": asset.purchase_date, + "asset_value": asset_value + } + data.append(row) return data diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 71231f68c8..75da71ceff 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -168,6 +168,7 @@ "bold": 1, "fieldname": "supplier", "fieldtype": "Link", + "in_global_search": 1, "in_standard_filter": 1, "label": "Supplier", "oldfieldname": "supplier", @@ -1106,7 +1107,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:58:14.697921", + "modified": "2020-12-03 16:46:44.229351", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index c427242208..e537771eaf 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -290,11 +290,17 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e dialog.show(); }, __("Get Items From")); + // Link Material Requests + this.frm.add_custom_button(__('Link to Material Requests'), + function() { + erpnext.buying.link_to_mrs(me.frm); + }, __("Tools")); + // Get Suppliers this.frm.add_custom_button(__('Get Suppliers'), function() { me.get_suppliers_button(me.frm); - }); + }, __("Tools")); } }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 3af6cf8e5d..4ce4100a7f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -18,7 +18,6 @@ "suppliers", "items_section", "items", - "link_to_mrs", "supplier_response_section", "salutation", "subject", @@ -118,13 +117,6 @@ "reqd": 1 }, { - "depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)", - "fieldname": "link_to_mrs", - "fieldtype": "Button", - "label": "Link to Material Requests" - }, - { - "depends_on": "eval:!doc.__islocal", "fieldname": "supplier_response_section", "fieldtype": "Section Break", "label": "Email Details" @@ -260,7 +252,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-04 22:04:29.017134", + "modified": "2020-11-05 22:04:29.017134", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index df143eefa0..0ee9d180d9 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -49,6 +49,12 @@ class Supplier(TransactionBase): msgprint(_("Series is mandatory"), raise_exception=1) validate_party_accounts(self) + self.validate_internal_supplier() + + def validate_internal_supplier(self): + if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Supplier for company {0} already exists").format( + frappe.bold(self.represents_company))) def on_trash(self): delete_contact_and_address('Supplier', self.name) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index a7cab5015e..a3b2085400 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -50,6 +50,12 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext }) }, __("Get Items From")); + // Link Material Requests + this.frm.add_custom_button(__('Link to Material Requests'), + function() { + erpnext.buying.link_to_mrs(me.frm); + }, __("Tools")); + this.frm.add_custom_button(__("Request for Quotation"), function() { if (!me.frm.doc.supplier) { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index b39c989073..40fbe2c26e 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -35,7 +35,6 @@ "ignore_pricing_rule", "items_section", "items", - "link_to_mrs", "pricing_rule_details", "pricing_rules", "section_break_22", @@ -322,12 +321,6 @@ "options": "Supplier Quotation Item", "reqd": 1 }, - { - "depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)", - "fieldname": "link_to_mrs", - "fieldtype": "Button", - "label": "Link to material requests" - }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", @@ -806,9 +799,10 @@ ], "icon": "fa fa-shopping-cart", "idx": 29, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:58:33.043971", + "modified": "2020-12-03 15:18:29.073368", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6108a614ca..32c5d3a3b1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +class AccountMissingError(frappe.ValidationError): pass + force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") class AccountsController(TransactionBase): @@ -105,6 +107,8 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.set_inter_company_account() + validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) @@ -735,6 +739,21 @@ class AccountsController(TransactionBase): return self._abbr + def raise_missing_debit_credit_account_error(self, party_type, party): + """Raise an error if debit to/credit to account does not exist.""" + db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") + rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" + + link_to_party = frappe.utils.get_link_to_form(party_type, party) + link_to_company = frappe.utils.get_link_to_form("Company", self.company) + + message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '') + message += "
" + _("Please set one of the following:") + "
" + message += "
" + + frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError) + def validate_party(self): party_type, party = self.get_party() validate_party_frozen_disabled(party_type, party) @@ -915,6 +934,38 @@ class AccountsController(TransactionBase): else: return frappe.db.get_single_value("Global Defaults", "disable_rounded_total") + def set_inter_company_account(self): + """ + Set intercompany account for inter warehouse transactions + This account will be used in case billing company and internal customer's + representation company is same + """ + + if self.is_internal_transfer() and not self.unrealized_profit_loss_account: + unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account') + + if not unrealized_profit_loss_account: + msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format( + frappe.bold(self.company)) + frappe.throw(msg) + + self.unrealized_profit_loss_account = unrealized_profit_loss_account + + def is_internal_transfer(self): + """ + It will an internal transfer if its an internal customer and representation + company is same as billing company + """ + if self.doctype == 'Sales Invoice': + internal_party_field = 'is_internal_customer' + else: + internal_party_field = 'is_internal_supplier' + + if self.get(internal_party_field) and (self.represents_company == self.company): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9ee83e3481..286c4f4451 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -42,6 +42,7 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() + self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -94,13 +95,23 @@ class BuyingController(StockController): def validate_stock_or_nonstock_items(self): if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items(): - tax_for_valuation = [d for d in self.get("taxes") + msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') + self.update_tax_category(msg) + + def update_tax_category_for_internal_transfer(self): + if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): + msg = _('Tax Category has been changed to "Total" as its an internal purchase.') + self.update_tax_category(msg) + + def update_tax_category(self, msg): + tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] - if tax_for_valuation: - for d in tax_for_valuation: - d.category = 'Total' - msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) + if tax_for_valuation: + for d in tax_for_valuation: + d.category = 'Total' + + msgprint(msg) def validate_asset_return(self): if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: @@ -497,6 +508,10 @@ class BuyingController(StockController): frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx)) d.stock_qty = flt(d.qty) * flt(d.conversion_factor) + if self.doctype=="Purchase Receipt" and d.meta.get_field("received_stock_qty"): + # Set Received Qty in Stock UOM + d.received_stock_qty = flt(d.received_qty) * flt(d.conversion_factor, d.precision("conversion_factor")) + def validate_purchase_return(self): for d in self.get("items"): if self.is_return and flt(d.rejected_qty) != 0: diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index afc5f8179f..5299b25601 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -203,10 +203,37 @@ def get_already_returned_items(doc): return items +def get_returned_qty_map_for_row(row_name, doctype): + child_doctype = doctype + " Item" + reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail" + + fields = [ + "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), + "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) + ] + + if doctype == "Purchase Receipt": + fields += [ + "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), + "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), + "sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype) + ] + + data = frappe.db.get_list(doctype, + fields = fields, + filters = [ + [doctype, "docstatus", "=", 1], + [doctype, "is_return", "=", 1], + [child_doctype, reference_field, "=", row_name] + ]) + + return data[0] + def make_return_doc(doctype, source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc company = frappe.db.get_value("Delivery Note", source_name, "company") default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return") + def set_missing_values(source, target): doc = frappe.get_doc(target) doc.is_return = 1 @@ -261,20 +288,25 @@ def make_return_doc(doctype, source_name, target_doc=None): doc.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): - target_doc.qty = -1* source_doc.qty + target_doc.qty = -1 * source_doc.qty + if doctype == "Purchase Receipt": - target_doc.received_qty = -1* source_doc.received_qty - target_doc.rejected_qty = -1* source_doc.rejected_qty - target_doc.qty = -1* source_doc.qty - target_doc.stock_qty = -1 * source_doc.stock_qty + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) + target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + target_doc.received_stock_qty = -1 * flt(source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0)) + target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": - target_doc.received_qty = -1* source_doc.received_qty - target_doc.rejected_qty = -1* source_doc.rejected_qty + target_doc.received_qty = -1 * source_doc.received_qty + target_doc.rejected_qty = -1 * source_doc.rejected_qty target_doc.qty = -1* source_doc.qty target_doc.stock_qty = -1 * source_doc.stock_qty target_doc.purchase_order = source_doc.purchase_order @@ -285,6 +317,10 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_invoice_item = source_doc.name elif doctype == "Delivery Note": + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_invoice = source_doc.against_sales_invoice target_doc.so_detail = source_doc.so_detail diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 515239a982..4dbd7bfa18 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -42,7 +42,7 @@ class SellingController(StockController): self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() - self.set_po_nos() + self.set_po_nos(for_validate=True) self.set_gross_profit() set_default_income_account_for_item(self) self.set_customer_address() @@ -370,20 +370,28 @@ class SellingController(StockController): })) self.make_sl_entries(sl_entries) - def set_po_nos(self): + def set_po_nos(self, for_validate=False): if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + if for_validate and self.po_no: + return self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): + if for_validate and self.po_no: + return self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): po_nos = [] + if self.po_no: + po_nos.append(self.po_no) self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] + if self.po_no: + po_nos.append(self.po_no) self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 9feac78770..8c05134ae4 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -58,6 +58,7 @@ status_map = { "Delivery Note": [ ["Draft", None], ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], + ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], @@ -65,6 +66,7 @@ status_map = { "Purchase Receipt": [ ["Draft", None], ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], + ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], @@ -232,7 +234,7 @@ class StatusUpdater(Document): self._update_children(args, update_modified) - if "percent_join_field" in args: + if "percent_join_field" in args or "percent_join_field_parent" in args: self._update_percent_field_in_targets(args, update_modified) def _update_children(self, args, update_modified): @@ -252,33 +254,43 @@ class StatusUpdater(Document): if not args.get("second_source_extra_cond"): args["second_source_extra_cond"] = "" - args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) + args['second_source_condition'] = frappe.db.sql(""" select 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 FOR UPDATE), 0)""" % args + and (`tab%(second_source_dt)s`.docstatus=1) + %(second_source_extra_cond)s), 0) """ % args)[0][0] if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" - frappe.db.sql("""update `tab%(target_dt)s` - set %(target_field)s = ( + args["source_dt_value"] = frappe.db.sql(""" (select ifnull(sum(%(source_field)s), 0) from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s" and (docstatus=1 %(cond)s) %(extra_cond)s) - %(second_source_condition)s - ) - %(update_modified)s + """ % args)[0][0] or 0.0 + + if args['second_source_condition']: + args["source_dt_value"] += flt(args['second_source_condition']) + + frappe.db.sql("""update `tab%(target_dt)s` + set %(target_field)s = %(source_dt_value)s %(update_modified)s where name='%(detail_id)s'""" % args) def _update_percent_field_in_targets(self, args, update_modified=True): """Update percent field in parent transaction""" - distinct_transactions = set([d.get(args['percent_join_field']) - for d in self.get_all_children(args['source_dt'])]) + if args.get('percent_join_field_parent'): + # if reference to target doc where % is to be updated, is + # in source doc's parent form, consider percent_join_field_parent + args['name'] = self.get(args['percent_join_field_parent']) + self._update_percent_field(args, update_modified) + else: + distinct_transactions = set([d.get(args['percent_join_field']) + for d in self.get_all_children(args['source_dt'])]) - for name in distinct_transactions: - if name: - args['name'] = name - self._update_percent_field(args, update_modified) + for name in distinct_transactions: + if name: + args['name'] = name + self._update_percent_field(args, update_modified) def _update_percent_field(self, args, update_modified=True): """Update percent field in parent transaction""" diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2d2fff8fd5..683d7f77b5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -77,7 +77,7 @@ class StockController(AccountsController): if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): - # from warehouse account/ target warehouse account + # from warehouse account self.check_expense_account(item_row) @@ -92,9 +92,16 @@ class StockController(AccountsController): sle = self.update_stock_ledger_entries(sle) + # expense account/ target_warehouse / source_warehouse + if item_row.get('target_warehouse'): + warehouse = item_row.get('target_warehouse') + expense_account = warehouse_account[warehouse]["account"] + else: + expense_account = item_row.expense_account + gl_list.append(self.get_gl_dict({ "account": warehouse_account[sle.warehouse]["account"], - "against": item_row.expense_account, + "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), "remarks": self.get("remarks") or "Accounting Entry for Stock", @@ -102,9 +109,8 @@ class StockController(AccountsController): "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - # expense account gl_list.append(self.get_gl_dict({ - "account": item_row.expense_account, + "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), @@ -340,11 +346,15 @@ class StockController(AccountsController): validate_warehouse_company(w, self.company) def update_billing_percentage(self, update_modified=True): + target_ref_field = "amount" + if self.doctype == "Delivery Note": + target_ref_field = "amount - (returned_qty * rate)" + self._update_percent_field({ "target_dt": self.doctype + " Item", "target_parent_dt": self.doctype, "target_parent_field": "per_billed", - "target_ref_field": "amount", + "target_ref_field": target_ref_field, "target_field": "billed_amt", "name": self.name, }, update_modified) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 81d07c1327..8dd2e5bacb 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -519,6 +519,17 @@ class calculate_taxes_and_totals(object): if self.doc.docstatus == 0: self.calculate_outstanding_amount() + def is_internal_invoice(self): + """ + Checks if its an internal transfer invoice + and decides if to calculate any out standing amount or not + """ + + if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer(): + return True + + return False + def calculate_outstanding_amount(self): # NOTE: # write_off_amount is only for POS Invoice @@ -526,7 +537,8 @@ 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 and not self.doc.get('is_pos'): return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \ + self.is_internal_invoice(): 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']) @@ -641,7 +653,8 @@ class calculate_taxes_and_totals(object): if default_mode_of_payment: self.doc.append('payments', { 'mode_of_payment': default_mode_of_payment.mode_of_payment, - 'amount': total_amount_to_pay + 'amount': total_amount_to_pay, + 'default': 1 }) else: self.doc.is_pos = 0 diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index ee9e895130..9968855163 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -1,23 +1,31 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -cur_frm.add_fetch("contract_template", "contract_terms", "contract_terms"); -cur_frm.add_fetch("contract_template", "requires_fulfilment", "requires_fulfilment"); - -// Add fulfilment terms from contract template into contract frappe.ui.form.on("Contract", { contract_template: function (frm) { - // Populate the fulfilment terms table from a contract template, if any if (frm.doc.contract_template) { - frappe.model.with_doc("Contract Template", frm.doc.contract_template, function () { - var tabletransfer = frappe.model.get_doc("Contract Template", frm.doc.contract_template); - - frm.doc.fulfilment_terms = []; - $.each(tabletransfer.fulfilment_terms, function (index, row) { - var d = frm.add_child("fulfilment_terms"); - d.requirement = row.requirement; - frm.refresh_field("fulfilment_terms"); - }); + frappe.call({ + method: 'erpnext.crm.doctype.contract_template.contract_template.get_contract_template', + args: { + template_name: frm.doc.contract_template, + doc: frm.doc + }, + callback: function(r) { + if (r && r.message) { + let contract_template = r.message.contract_template; + frm.set_value("contract_terms", r.message.contract_terms); + frm.set_value("requires_fulfilment", contract_template.requires_fulfilment); + + if (frm.doc.requires_fulfilment) { + // Populate the fulfilment terms table from a contract template, if any + r.message.contract_template.fulfilment_terms.forEach(element => { + let d = frm.add_child("fulfilment_terms"); + d.requirement = element.requirement; + }); + frm.refresh_field("fulfilment_terms"); + } + } + } }); } } diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 0026e4a02e..de3230f0e6 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2018-04-12 06:32:04.582486", @@ -247,7 +248,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-30 06:56:07.257932", + "modified": "2020-12-07 11:15:58.385521", "modified_by": "Administrator", "module": "CRM", "name": "Contract", diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index 5e4582f8d3..7cc5ec13cf 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -11,7 +11,9 @@ "contract_terms", "sb_fulfilment", "requires_fulfilment", - "fulfilment_terms" + "fulfilment_terms", + "section_break_6", + "contract_template_help" ], "fields": [ { @@ -41,10 +43,20 @@ "fieldtype": "Table", "label": "Fulfilment Terms and Conditions", "options": "Contract Template Fulfilment Terms" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "contract_template_help", + "fieldtype": "HTML", + "label": "Contract Template Help", + "options": "

Contract Template Example

\n\n
Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n
\n\n

How to get fieldnames

\n\n

The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

" } ], "links": [], - "modified": "2020-11-11 17:49:44.879363", + "modified": "2020-12-07 10:44:22.587047", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", diff --git a/erpnext/crm/doctype/contract_template/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py index 601ee9a28b..69fd86f7fb 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.py +++ b/erpnext/crm/doctype/contract_template/contract_template.py @@ -5,6 +5,27 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils.jinja import validate_template +from six import string_types +import json class ContractTemplate(Document): - pass + def validate(self): + if self.contract_terms: + validate_template(self.contract_terms) + +@frappe.whitelist() +def get_contract_template(template_name, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + contract_template = frappe.get_doc("Contract Template", template_name) + contract_terms = None + + if contract_template.contract_terms: + contract_terms = frappe.render_template(contract_template.contract_terms, doc) + + return { + 'contract_template': contract_template, + 'contract_terms': contract_terms + } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index efbaa71924..f0a05ed192 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -260,6 +260,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): """Shipping lines represents the shipping details, each such shipping detail consists of a list of tax_lines""" for shipping_charge in shipping_lines: + if shipping_charge.get("price"): + taxes.append({ + "charge_type": _("Actual"), + "account_head": get_tax_account_head(shipping_charge), + "description": shipping_charge["title"], + "tax_amount": shipping_charge["price"], + "cost_center": shopify_settings.cost_center + }) + for tax in shipping_charge.get("tax_lines"): taxes.append({ "charge_type": _("Actual"), diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index e278fd7807..362f6cf88e 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -60,4 +60,12 @@ def create_mode_of_payment(gateway, payment_type="General"): "default_account": payment_gateway_account }] }) - mode_of_payment.insert(ignore_permissions=True) \ No newline at end of file + mode_of_payment.insert(ignore_permissions=True) + +def get_tracking_url(carrier, tracking_number): + # Return the formatted Tracking URL. + tracking_url = '' + url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference') + if url_reference: + tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number}) + return tracking_url diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 81d60481ce..af601f3eb2 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -30,6 +30,11 @@ "label": "Laboratory", "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test\",\n\t\t\"label\": \"Lab Test\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sample Collection\",\n\t\t\"label\": \"Sample Collection\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Dosage Form\",\n\t\t\"label\": \"Dosage Form\"\n\t}\n]" }, + { + "hidden": 0, + "label": "Inpatient", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Order\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Entry\",\n\t\t\"label\": \"Inpatient Medication Entry\"\n\t}\n]" + }, { "hidden": 0, "label": "Rehabilitation and Physiotherapy", @@ -38,7 +43,7 @@ { "hidden": 0, "label": "Records and History", - "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]" + "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t}\n]" }, { "hidden": 0, @@ -64,7 +69,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-11-23 23:00:48.764377", + "modified": "2020-11-26 22:09:09.164584", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index cdf692e68b..7e7fd82411 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -7,6 +7,7 @@ import frappe import unittest from frappe.utils import nowdate, add_days from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_appointment, create_healthcare_service_items +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile test_dependencies = ["Company"] @@ -15,6 +16,7 @@ class TestFeeValidity(unittest.TestCase): frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabPatient`""") + make_pos_profile() def test_fee_validity(self): item = create_healthcare_service_items() diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js index f523cf21bd..ca97489b8d 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js @@ -29,6 +29,29 @@ frappe.ui.form.on('Inpatient Medication Entry', { } }; }); + + if (frm.doc.__islocal || frm.doc.docstatus !== 0 || !frm.doc.update_stock) + return; + + frm.add_custom_button(__('Make Stock Entry'), function() { + frappe.call({ + method: 'erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry.make_difference_stock_entry', + args: { docname: frm.doc.name }, + freeze: true, + callback: function(r) { + if (r.message) { + var doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } else { + frappe.msgprint({ + title: __('No Drug Shortage'), + message: __('All the drugs are available with sufficient qty to process this Inpatient Medication Entry.'), + indicator: 'green' + }); + } + } + }); + }); }, patient: function(frm) { diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index 5dac23abd9..70ae713866 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -142,25 +142,32 @@ class InpatientMedicationEntry(Document): return orders, order_entry_map def check_stock_qty(self): - from erpnext.stock.stock_ledger import NegativeStockError + drug_shortage = get_drug_shortage_map(self.medication_orders, self.warehouse) - drug_availability = dict() - for d in self.medication_orders: - if not drug_availability.get(d.drug_code): - drug_availability[d.drug_code] = 0 - drug_availability[d.drug_code] += flt(d.dosage) + if drug_shortage: + message = _('Quantity not available for the following items in warehouse {0}. ').format(frappe.bold(self.warehouse)) + message += _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.') - for drug, dosage in drug_availability.items(): - available_qty = get_latest_stock_qty(drug, self.warehouse) + formatted_item_rows = '' - # validate qty - if flt(available_qty) < flt(dosage): - frappe.throw(_('Quantity not available for {0} in warehouse {1}').format( - frappe.bold(drug), frappe.bold(self.warehouse)) - + '

' + _('Available quantity is {0}, you need {1}').format( - frappe.bold(available_qty), frappe.bold(dosage)) - + '

' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'), - NegativeStockError, title=_('Insufficient Stock')) + for drug, shortage_qty in drug_shortage.items(): + item_link = get_link_to_form('Item', drug) + formatted_item_rows += """ + + + """.format(item_link, frappe.bold(shortage_qty)) + + message += """ +
{%= __(range3) %} {%= __(range4) %} {%= __(range5) %}{%= __(range6) %} {%= __("Total") %}
{%= __("Total Outstanding") %}{%= format_number(balance_row["range1"], null, 2) %}{%= format_currency(balance_row["range2"]) %}{%= format_currency(balance_row["range3"]) %}{%= format_currency(balance_row["range4"]) %}{%= format_currency(balance_row["range5"]) %} + {%= format_number(balance_row["age"], null, 2) %} + + {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %} + {%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %} -
{%= __("Future Payments") %} {%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
{%= __("Total") %} - {%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %} - {%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} {%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} - {%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= data[i]["future_ref"] %}{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}{%= __("Total") %}{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}{%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}{%= format_currency(data[i]["paid"], data[i]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}
{0}{1}
+ + + + + {2} +
{0}{1}
+ """.format(_('Drug Code'), _('Shortage Qty'), formatted_item_rows) + + frappe.throw(message, title=_('Insufficient Stock'), is_minimizable=True, wide=True) def make_stock_entry(self): stock_entry = frappe.new_doc('Stock Entry') @@ -223,7 +230,8 @@ def get_pending_medication_orders(entry): for doc in data: inpatient_record = doc.inpatient_record - doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record) + if inpatient_record: + doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record) if entry.service_unit and doc.service_unit != entry.service_unit: to_remove.append(doc) @@ -276,4 +284,55 @@ def get_current_healthcare_service_unit(inpatient_record): ip_record = frappe.get_doc('Inpatient Record', inpatient_record) if ip_record.inpatient_occupancies: return ip_record.inpatient_occupancies[-1].service_unit - return \ No newline at end of file + return + + +def get_drug_shortage_map(medication_orders, warehouse): + """ + Returns a dict like { drug_code: shortage_qty } + """ + drug_requirement = dict() + for d in medication_orders: + if not drug_requirement.get(d.drug_code): + drug_requirement[d.drug_code] = 0 + drug_requirement[d.drug_code] += flt(d.dosage) + + drug_shortage = dict() + for drug, required_qty in drug_requirement.items(): + available_qty = get_latest_stock_qty(drug, warehouse) + if flt(required_qty) > flt(available_qty): + drug_shortage[drug] = flt(flt(required_qty) - flt(available_qty)) + + return drug_shortage + + +@frappe.whitelist() +def make_difference_stock_entry(docname): + doc = frappe.get_doc('Inpatient Medication Entry', docname) + drug_shortage = get_drug_shortage_map(doc.medication_orders, doc.warehouse) + + if not drug_shortage: + return None + + stock_entry = frappe.new_doc('Stock Entry') + stock_entry.purpose = 'Material Transfer' + stock_entry.set_stock_entry_type() + stock_entry.to_warehouse = doc.warehouse + stock_entry.company = doc.company + cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center') + expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company) + + for drug, shortage_qty in drug_shortage.items(): + se_child = stock_entry.append('items') + se_child.item_code = drug + se_child.item_name = frappe.db.get_value('Item', drug, 'stock_uom') + se_child.uom = frappe.db.get_value('Item', drug, 'stock_uom') + se_child.stock_uom = se_child.uom + se_child.qty = flt(shortage_qty) + se_child.t_warehouse = doc.warehouse + # in stock uom + se_child.conversion_factor = 1 + se_child.cost_center = cost_center + se_child.expense_account = expense_account + + return stock_entry diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py index 2f1bb6b56f..7cb5a4814e 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -9,6 +9,7 @@ from frappe.utils import add_days, getdate, now_datetime from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme +from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_drug_shortage_map, make_difference_stock_entry from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account class TestInpatientMedicationEntry(unittest.TestCase): @@ -82,6 +83,39 @@ class TestInpatientMedicationEntry(unittest.TestCase): self.assertEqual(stock_entry.items[0].patient, self.patient) self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name) + def test_drug_shortage_stock_entry(self): + ipmo = create_ipmo(self.patient) + ipmo.submit() + ipmo.reload() + + date = add_days(getdate(), -1) + filters = frappe._dict( + from_date=date, + to_date=date, + from_time='', + to_time='', + item_code='Dextromethorphan', + patient=self.patient + ) + + # check drug shortage + ipme = create_ipme(filters, update_stock=1) + ipme.warehouse = 'Finished Goods - _TC' + ipme.save() + drug_shortage = get_drug_shortage_map(ipme.medication_orders, ipme.warehouse) + self.assertEqual(drug_shortage.get('Dextromethorphan'), 3) + + # check material transfer for drug shortage + make_stock_entry() + stock_entry = make_difference_stock_entry(ipme.name) + self.assertEqual(stock_entry.items[0].item_code, 'Dextromethorphan') + self.assertEqual(stock_entry.items[0].qty, 3) + stock_entry.from_warehouse = 'Stores - _TC' + stock_entry.submit() + + ipme.reload() + ipme.submit() + def tearDown(self): # cleanup - Discharge schedule_discharge(frappe.as_json({'patient': self.patient})) @@ -94,15 +128,12 @@ class TestInpatientMedicationEntry(unittest.TestCase): for entry in frappe.get_all('Inpatient Medication Entry'): doc = frappe.get_doc('Inpatient Medication Entry', entry.name) doc.cancel() - frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name}) - doc.delete() for entry in frappe.get_all('Inpatient Medication Order'): doc = frappe.get_doc('Inpatient Medication Order', entry.name) doc.cancel() - doc.delete() -def make_stock_entry(): +def make_stock_entry(warehouse=None): frappe.db.set_value('Company', '_Test Company', { 'stock_adjustment_account': 'Stock Adjustment - _TC', 'default_inventory_account': 'Stock In Hand - _TC' @@ -110,7 +141,7 @@ def make_stock_entry(): stock_entry = frappe.new_doc('Stock Entry') stock_entry.stock_entry_type = 'Material Receipt' stock_entry.company = '_Test Company' - stock_entry.to_warehouse = 'Stores - _TC' + stock_entry.to_warehouse = warehouse or 'Stores - _TC' expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company') se_child = stock_entry.append('items') se_child.item_code = 'Dextromethorphan' diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py index e3def72334..39603f77a0 100644 --- a/erpnext/healthcare/doctype/patient/patient_dashboard.py +++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py @@ -18,6 +18,10 @@ def get_data(): { 'label': _('Billing'), 'items': ['Sales Invoice'] + }, + { + 'label': _('Orders'), + 'items': ['Inpatient Medication Order'] } ] } diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index eeed157291..3df7ba1531 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -7,12 +7,14 @@ import frappe from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter from frappe.utils import nowdate, add_days from frappe.utils.make_random import get_random +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPatientAppointment(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabPatient Encounter`""") + make_pos_profile() def test_status(self): patient, medical_department, practitioner = create_healthcare_docs() diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index aa85a23113..419d956425 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -6,11 +6,13 @@ import unittest import frappe from frappe.utils import nowdate from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPatientMedicalRecord(unittest.TestCase): def setUp(self): frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + make_pos_profile() def test_medical_record(self): patient, medical_department, practitioner = create_healthcare_docs() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 987345697a..1e3bb6a5cf 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -441,42 +441,43 @@ global_search_doctypes = { {"doctype": "Sales Order", "index": 8}, {"doctype": "Quotation", "index": 9}, {"doctype": "Work Order", "index": 10}, - {"doctype": "Purchase Receipt", "index": 11}, - {"doctype": "Purchase Invoice", "index": 12}, - {"doctype": "Delivery Note", "index": 13}, - {"doctype": "Stock Entry", "index": 14}, - {"doctype": "Material Request", "index": 15}, - {"doctype": "Delivery Trip", "index": 16}, - {"doctype": "Pick List", "index": 17}, - {"doctype": "Salary Slip", "index": 18}, - {"doctype": "Leave Application", "index": 19}, - {"doctype": "Expense Claim", "index": 20}, - {"doctype": "Payment Entry", "index": 21}, - {"doctype": "Lead", "index": 22}, - {"doctype": "Opportunity", "index": 23}, - {"doctype": "Item Price", "index": 24}, - {"doctype": "Purchase Taxes and Charges Template", "index": 25}, - {"doctype": "Sales Taxes and Charges", "index": 26}, - {"doctype": "Asset", "index": 27}, - {"doctype": "Project", "index": 28}, - {"doctype": "Task", "index": 29}, - {"doctype": "Timesheet", "index": 30}, - {"doctype": "Issue", "index": 31}, - {"doctype": "Serial No", "index": 32}, - {"doctype": "Batch", "index": 33}, - {"doctype": "Branch", "index": 34}, - {"doctype": "Department", "index": 35}, - {"doctype": "Employee Grade", "index": 36}, - {"doctype": "Designation", "index": 37}, - {"doctype": "Job Opening", "index": 38}, - {"doctype": "Job Applicant", "index": 39}, - {"doctype": "Job Offer", "index": 40}, - {"doctype": "Salary Structure Assignment", "index": 41}, - {"doctype": "Appraisal", "index": 42}, - {"doctype": "Loan", "index": 43}, - {"doctype": "Maintenance Schedule", "index": 44}, - {"doctype": "Maintenance Visit", "index": 45}, - {"doctype": "Warranty Claim", "index": 46}, + {"doctype": "Purchase Order", "index": 11}, + {"doctype": "Purchase Receipt", "index": 12}, + {"doctype": "Purchase Invoice", "index": 13}, + {"doctype": "Delivery Note", "index": 14}, + {"doctype": "Stock Entry", "index": 15}, + {"doctype": "Material Request", "index": 16}, + {"doctype": "Delivery Trip", "index": 17}, + {"doctype": "Pick List", "index": 18}, + {"doctype": "Salary Slip", "index": 19}, + {"doctype": "Leave Application", "index": 20}, + {"doctype": "Expense Claim", "index": 21}, + {"doctype": "Payment Entry", "index": 22}, + {"doctype": "Lead", "index": 23}, + {"doctype": "Opportunity", "index": 24}, + {"doctype": "Item Price", "index": 25}, + {"doctype": "Purchase Taxes and Charges Template", "index": 26}, + {"doctype": "Sales Taxes and Charges", "index": 27}, + {"doctype": "Asset", "index": 28}, + {"doctype": "Project", "index": 29}, + {"doctype": "Task", "index": 30}, + {"doctype": "Timesheet", "index": 31}, + {"doctype": "Issue", "index": 32}, + {"doctype": "Serial No", "index": 33}, + {"doctype": "Batch", "index": 34}, + {"doctype": "Branch", "index": 35}, + {"doctype": "Department", "index": 36}, + {"doctype": "Employee Grade", "index": 37}, + {"doctype": "Designation", "index": 38}, + {"doctype": "Job Opening", "index": 39}, + {"doctype": "Job Applicant", "index": 40}, + {"doctype": "Job Offer", "index": 41}, + {"doctype": "Salary Structure Assignment", "index": 42}, + {"doctype": "Appraisal", "index": 43}, + {"doctype": "Loan", "index": 44}, + {"doctype": "Maintenance Schedule", "index": 45}, + {"doctype": "Maintenance Visit", "index": 46}, + {"doctype": "Warranty Claim", "index": 47}, ], "Healthcare": [ {'doctype': 'Patient', 'index': 1}, diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 7056adf208..5037ceb489 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -18,13 +18,18 @@ frappe.ui.form.on('Employee Advance', { if (!frm.doc.employee) { frappe.msgprint(__("Please select employee first")); } - var company_currency = erpnext.get_currency(frm.doc.company); + let company_currency = erpnext.get_currency(frm.doc.company); + let currencies = [company_currency]; + if (frm.doc.currency && (frm.doc.currency != company_currency)) { + currencies.push(frm.doc.currency); + } + return { filters: { "root_type": "Asset", "is_group": 0, "company": frm.doc.company, - "account_currency": ["in", [frm.doc.currency, company_currency]], + "account_currency": ["in", currencies], } }; }); @@ -181,21 +186,23 @@ frappe.ui.form.on('Employee Advance', { }, currency: function(frm) { - var from_currency = frm.doc.currency; - var company_currency; - if (!frm.doc.company) { - company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); - } else { - company_currency = erpnext.get_currency(frm.doc.company); + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (from_currency != company_currency) { + frm.events.set_exchange_rate(frm, from_currency, company_currency); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + frm.refresh_fields(); } - if (from_currency != company_currency) { - frm.events.set_exchange_rate(frm, from_currency, company_currency); - } else { - frm.set_value("exchange_rate", 1.0); - frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); - } - frm.refresh_fields(); }, set_exchange_rate: function(frm, from_currency, company_currency) { diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e53927918e..2bf3fbf75e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -491,6 +491,39 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_partial_material_consumption(self): + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) + + ste_cancel_list = [] + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + + ste_cancel_list.extend([ste1, ste2]) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4)) + s.submit() + ste_cancel_list.append(s) + + ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2)) + ste1.submit() + ste_cancel_list.append(ste1) + + print(wo_order.name) + ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) + self.assertEquals(ste3.fg_completed_qty, 2) + + expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4} + for row in ste3.items: + self.assertEquals(row.qty, expected_qty.get(row.item_code)) + + for ste_doc in ste_cancel_list: + ste_doc.cancel() + + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9ce465ccaf..a6086fb88d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -545,7 +545,8 @@ erpnext.work_order = { var tbl = frm.doc.required_items || []; var tbl_lenght = tbl.length; for (var i = 0, len = tbl_lenght; i < len; i++) { - if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) { + let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { counter += 1; } } diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index ebc01c65af..806d268ffd 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -124,7 +124,7 @@ class ProductionPlanReport(object): if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)") raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code, - bom_item.item_name as raw_material_name, {0} as required_qty + bom_item.item_name as raw_material_name, {0} as required_qty_per_unit FROM `tabBOM` as bom, `tab{1}` as bom_item WHERE @@ -208,7 +208,7 @@ class ProductionPlanReport(object): warehouses = self.mrp_warehouses or [] for d in self.raw_materials_dict.get(key): if self.filters.based_on != "Work Order": - d.required_qty = d.required_qty * data.qty_to_manufacture + d.required_qty = d.required_qty_per_unit * data.qty_to_manufacture if not warehouses: warehouses = [data.warehouse] diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 44b975e9e9..25d6b53830 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -59,7 +59,7 @@ class Member(Document): frappe.msgprint(_("A customer is already linked to this Member")) cust = create_customer(frappe._dict({ 'fullname': self.member_name, - 'email': self.email_id or self.user, + 'email': self.email_id or self.email, 'phone': None })) @@ -177,4 +177,4 @@ def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, m mobile=mobile )) - return member.name \ No newline at end of file + return member.name diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 61aa2eec59..86ac613ae5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -691,6 +691,7 @@ erpnext.patches.v13_0.update_old_loans erpnext.patches.v12_0.set_serial_no_status #2020-05-21 erpnext.patches.v12_0.update_price_list_currency_in_bom execute:frappe.reload_doctype('Dashboard') +execute:frappe.reload_doc('desk', 'doctype', 'number_card_link') execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25 @@ -738,3 +739,5 @@ erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy +erpnext.patches.v13_0.add_po_to_global_search +erpnext.patches.v13_0.update_returned_qty_in_pr_dn diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py new file mode 100644 index 0000000000..1c60b18e5b --- /dev/null +++ b/erpnext/patches/v13_0/add_po_to_global_search.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import frappe + + +def execute(): + global_search_settings = frappe.get_single("Global Search Settings") + + if "Purchase Order" in ( + dt.document_type for dt in global_search_settings.allowed_in_global_search + ): + return + + global_search_settings.append( + "allowed_in_global_search", {"document_type": "Purchase Order"} + ) + + global_search_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py index 80c9137653..90dc0e2e18 100644 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -52,6 +52,8 @@ def create_assignment(employee, leave_policy, leave_period=None, allocation_exis if leave_period: filters["leave_period"] = leave_period + frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') + if not frappe.db.exists("Leave Policy Assignment" , filters): lpa = frappe.new_doc("Leave Policy Assignment") lpa.employee = employee diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index 96a63623c0..fa1dfed643 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -29,7 +29,7 @@ def execute(): 'response_by_variance': response_by_variance, 'resolution_by_variance': resolution_by_variance, 'first_response_time': mins_to_first_response - }) + }, update_modified=False) # commit after every 100 updates count += 1 if count%100 == 0: @@ -44,7 +44,7 @@ def execute(): count = 0 for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response) + frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False) # commit after every 100 updates count += 1 if count%100 == 0: diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 77239429c5..561e967d6d 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -5,6 +5,8 @@ from frappe.utils import nowdate from erpnext.accounts.doctype.account.test_account import create_account from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.loan.loan import make_repayment_entry +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import get_accrued_interest_entries +from frappe.model.naming import make_autoname def execute(): @@ -18,15 +20,29 @@ def execute(): frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail') frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual') frappe.reload_doc('accounts', 'doctype', 'gl_entry') + frappe.reload_doc('accounts', 'doctype', 'journal_entry_account') updated_loan_types = [] + loans_to_close = [] + + # Update old loan status as closed + if frappe.db.has_column('Repayment Schedule', 'paid'): + loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule` + where paid = 0 and docstatus = 1""", as_dict=1) + + loans_to_close = [d.parent for d in loans_list] + + if loans_to_close: + frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close)) loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment', - 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account']) + 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'], + filters={'docstatus': 1, 'status': ('!=', 'Closed')}) for loan in loans: # Update details in Loan Types and Loan loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company') + loan_type = loan.loan_type group_income_account = frappe.get_value('Account', {'company': loan.company, 'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')}) @@ -38,7 +54,26 @@ def execute(): penalty_account = create_account(company=loan.company, account_type='Income Account', account_name='Penalty Account', parent_account=group_income_account) - if not loan_type_company: + # Same loan type used for multiple companies + if loan_type_company and loan_type_company != loan.company: + # get loan type for appropriate company + loan_type_name = frappe.get_value('Loan Type', {'company': loan.company, + 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account, + 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account, + 'penalty_income_account': loan.penalty_income_account}, 'name') + + if not loan_type_name: + loan_type_name = create_loan_type(loan, loan_type_name, penalty_account) + + # update loan type in loan + frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, + loan.name)) + + loan_type = loan_type_name + if loan_type_name not in updated_loan_types: + updated_loan_types.append(loan_type_name) + + elif not loan_type_company: loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type) loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company @@ -49,8 +84,9 @@ def execute(): loan_type_doc.penalty_income_account = penalty_account loan_type_doc.submit() updated_loan_types.append(loan.loan_type) + loan_type = loan.loan_type - if loan.loan_type in updated_loan_types: + if loan_type in updated_loan_types: if loan.status == 'Fully Disbursed': status = 'Disbursed' elif loan.status == 'Repaid/Closed': @@ -64,25 +100,48 @@ def execute(): 'status': status }) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type, + process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type, loan=loan.name) - payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date - FROM `tabJournal Entry` j, `tabJournal Entry Account` a - WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s - and account = %s - ''', (loan.name, loan.loan_account), as_dict=1) - for payment in payments: - repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant, - loan.loan_type, loan.company) + if frappe.db.has_column('Repayment Schedule', 'paid'): + total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name}, + ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest']) - repayment_entry.amount_paid = payment.debit_in_account_currency - repayment_entry.posting_date = payment.posting_date - repayment_entry.save() - repayment_entry.submit() + accrued_entries = get_accrued_interest_entries(loan.name) + for entry in accrued_entries: + interest_paid = 0 + principal_paid = 0 - jv = frappe.get_doc('Journal Entry', payment.name) - jv.flags.ignore_links = True - jv.cancel() + if total_interest > entry.interest_amount: + interest_paid = entry.interest_amount + else: + interest_paid = total_interest + if total_principal > entry.payable_principal_amount: + principal_paid = entry.payable_principal_amount + else: + principal_paid = total_principal + + frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + SET paid_principal_amount = `paid_principal_amount` + %s, + paid_interest_amount = `paid_interest_amount` + %s + WHERE name = %s""", + (principal_paid, interest_paid, entry.name)) + + total_principal -= principal_paid + total_interest -= interest_paid + +def create_loan_type(loan, loan_type_name, penalty_account): + loan_type_doc = frappe.new_doc('Loan Type') + loan_type_doc.loan_name = make_autoname("Loan Type-.####") + loan_type_doc.is_term_loan = 1 + loan_type_doc.company = loan.company + loan_type_doc.mode_of_payment = loan.mode_of_payment + loan_type_doc.payment_account = loan.payment_account + loan_type_doc.loan_account = loan.loan_account + loan_type_doc.interest_income_account = loan.interest_income_account + loan_type_doc.penalty_income_account = penalty_account + loan_type_doc.submit() + + return loan_type_doc.name diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py new file mode 100644 index 0000000000..7f42cd92e3 --- /dev/null +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -0,0 +1,27 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('stock', 'doctype', 'purchase_receipt') + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') + frappe.reload_doc('stock', 'doctype', 'delivery_note') + frappe.reload_doc('stock', 'doctype', 'delivery_note_item') + + def update_from_return_docs(doctype): + for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}): + # Update original receipt/delivery document from return + return_doc = frappe.get_cached_doc(doctype, return_doc.name) + return_doc.update_prevdoc_status() + return_against = frappe.get_doc(doctype, return_doc.return_against) + return_against.update_billing_status() + + # Set received qty in stock uom in PR, as returned qty is checked against it + frappe.db.sql(""" update `tabPurchase Receipt Item` + set received_stock_qty = received_qty * conversion_factor + where docstatus = 1 """) + + for doctype in ('Purchase Receipt', 'Delivery Note'): + update_from_return_docs(doctype) \ No newline at end of file diff --git a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py index 6e92ffb8a0..910814fd22 100644 --- a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py +++ b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py @@ -7,19 +7,23 @@ import frappe def execute(): parent_list = [] count = 0 - for data in frappe.db.sql(""" - select + + frappe.reload_doc('stock', 'doctype', 'purchase_receipt') + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') + + for data in frappe.db.sql(""" + select `tabPurchase Receipt Item`.purchase_order, `tabPurchase Receipt Item`.name, `tabPurchase Receipt Item`.item_code, `tabPurchase Receipt Item`.idx, `tabPurchase Receipt Item`.parent - from + from `tabPurchase Receipt Item`, `tabPurchase Receipt` where `tabPurchase Receipt Item`.parent = `tabPurchase Receipt`.name and `tabPurchase Receipt Item`.purchase_order_item is null and `tabPurchase Receipt Item`.purchase_order is not null and `tabPurchase Receipt`.is_return = 1""", as_dict=1): - name = frappe.db.get_value('Purchase Order Item', + name = frappe.db.get_value('Purchase Order Item', {'item_code': data.item_code, 'parent': data.purchase_order, 'idx': data.idx}, 'name') if name: diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index 0784de93eb..7737e6c886 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,14 +12,6 @@ frappe.ui.form.on('Additional Salary', { } }; }); - - if (!frm.doc.currency) return; - frm.set_query("salary_component", function() { - return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {currency: frm.doc.currency, company: frm.doc.company} - }; - }); }, employee: function(frm) { diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index 9a5a463152..4c45580bf0 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -23,6 +23,7 @@ "employee_benefits", "totals", "total_amount", + "column_break", "pro_rata_dispensed_amount" ], "fields": [ @@ -139,11 +140,15 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-11-25 11:49:05.095101", + "modified": "2020-12-14 15:52:08.566418", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index 85d1c54a22..182ce0f83a 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -11,11 +11,11 @@ frappe.ui.form.on('Employee Incentive', { }; }); - if (!frm.doc.currency) return; + if (!frm.doc.company) return; frm.set_query("salary_component", function() { return { query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} + filters: {type: "earning", company: frm.doc.company} }; }); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 67ee231e40..8c2d9740ec 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -289,7 +289,9 @@ class PayrollEntry(Document): jv_name = journal_entry.name self.update_salary_slip_status(jv_name = jv_name) except Exception as e: - frappe.msgprint(e) + if type(e) in (str, list, tuple): + frappe.msgprint(e) + raise return jv_name diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js index 6fe8ccad46..f8bb40a9cb 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js @@ -4,9 +4,13 @@ frappe.ui.form.on('Retention Bonus', { setup: function(frm) { frm.set_query("employee", function() { + if (!frm.doc.company) { + frappe.msgprint(__("Please Select Company First")); + } return { filters: { - "status": "Active" + "status": "Active", + "company": frm.doc.company } }; }); diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 71cb4083ed..5daf1d439d 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -118,11 +118,6 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) - #Gross pay calculation based on attendances - gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) - - self.assertEqual(flt(ss.gross_pay, 2), flt(gross_pay, 2)) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") def test_salary_slip_with_holidays_included(self): diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index 7daae49c58..ba824c5d6f 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -55,17 +55,17 @@ frappe.ui.form.on('Salary Structure', { }, set_earning_deduction_component: function(frm) { - if(!frm.doc.currency && !frm.doc.company) return; + if(!frm.doc.company) return; frm.set_query("salary_component", "earnings", function() { return { query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} + filters: {type: "earning", company: frm.doc.company} }; }); frm.set_query("salary_component", "deductions", function() { return { query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company} + filters: {type: "deduction", company: frm.doc.company} }; }); }, @@ -74,7 +74,6 @@ frappe.ui.form.on('Salary Structure', { currency: function(frm) { calculate_totals(frm.doc); frm.trigger("set_dynamic_labels") - frm.trigger('set_earning_deduction_component'); frm.refresh() }, diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 877e41d93c..77914bb531 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -210,7 +210,7 @@ def get_employees(salary_structure): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): - if len(filters) < 3: + if len(filters) < 2: return {} return frappe.db.sql(""" diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 3f5652aa5d..db85a3ec99 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -189,6 +189,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ frappe.model.round_floats_in(item, ["qty", "received_qty"]); item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); + item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); } this._super(doc, cdt, cdn); @@ -293,69 +294,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this.get_terms(); }, - link_to_mrs: function() { - var my_items = []; - for (var i in cur_frm.doc.items) { - if(!cur_frm.doc.items[i].material_request){ - my_items.push(cur_frm.doc.items[i].item_code); - } - } - frappe.call({ - method: "erpnext.buying.utils.get_linked_material_requests", - args:{ - items: my_items - }, - callback: function(r) { - if(!r.message || r.message.length == 0) { - frappe.throw(__("No pending Material Requests found to link for the given items.")) - } - else { - var i = 0; - var item_length = cur_frm.doc.items.length; - while (i < item_length) { - var qty = cur_frm.doc.items[i].qty; - (r.message[0] || []).forEach(function(d) { - if (d.qty > 0 && qty > 0 && cur_frm.doc.items[i].item_code == d.item_code && !cur_frm.doc.items[i].material_request_item) - { - cur_frm.doc.items[i].material_request = d.mr_name; - cur_frm.doc.items[i].material_request_item = d.mr_item; - var my_qty = Math.min(qty, d.qty); - qty = qty - my_qty; - d.qty = d.qty - my_qty; - cur_frm.doc.items[i].stock_qty = my_qty*cur_frm.doc.items[i].conversion_factor; - cur_frm.doc.items[i].qty = my_qty; - - frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + cur_frm.doc.items[i].idx + ")"); - if (qty > 0) - { - frappe.msgprint("Splitting " + qty + " units of " + d.item_code); - var newrow = frappe.model.add_child(cur_frm.doc, cur_frm.doc.items[i].doctype, "items"); - item_length++; - - for (var key in cur_frm.doc.items[i]) - { - newrow[key] = cur_frm.doc.items[i][key]; - } - - newrow.idx = item_length; - newrow["stock_qty"] = newrow.conversion_factor*qty; - newrow["qty"] = qty; - - newrow["material_request"] = ""; - newrow["material_request_item"] = ""; - - } - } - }); - i++; - } - refresh_field("items"); - //cur_frm.save(); - } - } - }); - }, - update_auto_repeat_reference: function(doc) { if (doc.auto_repeat) { frappe.call({ @@ -421,6 +359,62 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ cur_frm.add_fetch('project', 'cost_center', 'cost_center'); +erpnext.buying.link_to_mrs = function(frm) { + frappe.call({ + method: "erpnext.buying.utils.get_linked_material_requests", + args:{ + items: frm.doc.items.map((item) => item.item_code) + }, + callback: function(r) { + if (!r.message || r.message.length == 0) { + frappe.throw({ + message: __("No pending Material Requests found to link for the given items."), + title: __("Note") + }); + } + + var item_length = frm.doc.items.length; + for (let item of frm.doc.items) { + var qty = item.qty; + (r.message[0] || []).forEach(function(d) { + if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item) + { + item.material_request = d.mr_name; + item.material_request_item = d.mr_item; + var my_qty = Math.min(qty, d.qty); + qty = qty - my_qty; + d.qty = d.qty - my_qty; + item.stock_qty = my_qty*item.conversion_factor; + item.qty = my_qty; + + frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")"); + if (qty > 0) + { + frappe.msgprint("Splitting " + qty + " units of " + d.item_code); + var newrow = frappe.model.add_child(frm.doc, item.doctype, "items"); + item_length++; + + for (var key in item) + { + newrow[key] = item[key]; + } + + newrow.idx = item_length; + newrow["stock_qty"] = newrow.conversion_factor*qty; + newrow["qty"] = qty; + + newrow["material_request"] = ""; + newrow["material_request_item"] = ""; + + } + } + }); + } + refresh_field("items"); + } + }); +} + erpnext.buying.get_default_bom = function(frm) { $.each(frm.doc["items"] || [], function(i, d) { if (d.item_code && d.bom === "") { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 99f3995a66..22e75780b8 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -609,6 +609,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_outstanding_amount(update_paid_amount); }, + is_internal_invoice: function() { + if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { + if (this.frm.doc.company === this.frm.doc.represents_company) { + return true; + } + } + return false; + }, + calculate_outstanding_amount: function(update_paid_amount) { // NOTE: // paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice @@ -617,7 +626,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_paid_amount(); } - if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return; + if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return; frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5abee50153..b9d884ff70 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -402,7 +402,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ show_description(row_to_modify.idx, row_to_modify.item_code); - this.frm.from_barcode = true; + this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1; frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, { item_code: data.item_code, qty: (row_to_modify.qty || 0) + 1 @@ -486,7 +486,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ d.item_code = ""; } - this.frm.from_barcode = true; + this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1; this.item_code(doc, cdt, cdn); }, @@ -503,11 +503,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ show_batch_dialog = 1; } // clear barcode if setting item (else barcode will take priority) - if(!this.frm.from_barcode) { + if (this.frm.from_barcode == 0) { item.barcode = null; } + this.frm.from_barcode = this.frm.from_barcode - 1 >= 0 ? this.frm.from_barcode - 1 : 0; + - this.frm.from_barcode = false; if(item.item_code || item.barcode || item.serial_no) { if(!this.validate_company_and_party()) { this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove(); diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index bd7f890306..f9caadeed7 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -20,4 +20,4 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 787d557e80..68c8a0d4d3 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -192,19 +192,20 @@ class GSTR3BReport(Document): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_type_map.get(d["ty"]) - gst_category = ["Registered Regular"] if d["ty"] == 'ISRC': - reverse_charge = "Y" + reverse_charge = ["Y"] itc_type = 'All Other ITC' gst_category = ['Unregistered', 'Overseas'] else: - reverse_charge = "N" + gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] + reverse_charge = ["N", "Y"] for account_head in self.account_heads: for category in gst_category: - for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: - d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) + for charge_type in reverse_charge: + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2) for key in ['iamt', 'camt', 'samt', 'csamt']: net_itc[key] += flt(d[key], 2) @@ -264,7 +265,8 @@ class GSTR3BReport(Document): def get_itc_details(self): itc_amount = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, + t.account_head, s.eligibility_for_itc, s.reverse_charge from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t where s.docstatus = 1 and t.parent = s.name and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s @@ -387,7 +389,7 @@ class GSTR3BReport(Document): tax_template = 'Purchase Taxes and Charges' tax_amounts = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head from `tab{doctype}` s , `tab{template}` t where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 3c156479c5..87baece65d 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { tax_category: function(frm) { frm.trigger('get_tax_template'); }, + customer_address: function(frm) { + frm.trigger('get_tax_template'); + }, get_tax_template: function(frm) { if (!frm.doc.company) return; @@ -19,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { 'shipping_address': frm.doc.shipping_address || '', 'shipping_address_name': frm.doc.shipping_address_name || '', 'customer_address': frm.doc.customer_address || '', + 'supplier_address': frm.doc.supplier_address, 'customer': frm.doc.customer, 'supplier': frm.doc.supplier, 'supplier_gstin': frm.doc.supplier_gstin, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 62487ba2aa..f8520c2d00 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -12,6 +12,7 @@ from erpnext.regional.india import number_state_mapping from six import string_types from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.utils import get_account_currency +from frappe.model.utils import get_fetch_values def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -161,6 +162,8 @@ def get_regional_address_details(party_details, doctype, company): party_details = json.loads(party_details) party_details = frappe._dict(party_details) + update_party_details(party_details, doctype) + party_details.place_of_supply = get_place_of_supply(party_details, doctype) if is_internal_transfer(party_details, doctype): @@ -209,6 +212,11 @@ def get_regional_address_details(party_details, doctype, company): return party_details +def update_party_details(party_details, doctype): + for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']: + if party_details.get(address_field): + party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field))) + def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): destination_gstin = party_details.company_gstin diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 837929709e..ad3de5f398 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -151,6 +151,7 @@ class Gstr1Report(object): {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} + and is_opening = 'No' order by posting_date desc """.format(select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions), self.filters, as_dict=1) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0172d9c128..29214ee06d 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -58,6 +58,7 @@ class Customer(TransactionBase): self.set_loyalty_program() self.check_customer_group_change() self.validate_default_bank_account() + self.validate_internal_customer() # set loyalty program tier if frappe.db.exists('Customer', self.name): @@ -82,6 +83,11 @@ class Customer(TransactionBase): if not is_company_account: frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + def validate_internal_customer(self): + if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company))) + def on_update(self): self.validate_name_with_customer_group() self.create_primary_contact() @@ -398,7 +404,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, # form a list of emails and names to show to the user credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) + frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) message = """Please contact any of the following users to extend the credit limits for {0}:

""".format(customer, '
  • '.join(credit_controller_users_formatted)) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 1d890bb91a..d4fb07cc27 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Sales Order", { frm.custom_make_buttons = { 'Delivery Note': 'Delivery Note', 'Pick List': 'Pick List', - 'Sales Invoice': 'Invoice', + 'Sales Invoice': 'Sales Invoice', 'Material Request': 'Material Request', 'Purchase Order': 'Purchase Order', 'Project': 'Project', diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 04d85e575c..accf59ebc4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -418,8 +418,7 @@ class SalesOrder(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): - delivery_date = get_next_schedule_date(ref_doc_delivery_date, - auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day)) + delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) if delivery_date <= transaction_date: delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ad1633e71d..d4cde43359 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -111,24 +111,24 @@ erpnext.PointOfSale.Controller = class { dialog.show(); } - prepare_app_defaults(data) { + async prepare_app_defaults(data) { this.pos_opening = data.name; this.company = data.company; this.pos_profile = data.pos_profile; this.pos_opening_time = data.period_start_date; + this.item_stock_map = {}; + this.settings = {}; frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - this.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.cart.make_customer_selector(); + this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); + this.settings.hide_images = profile.hide_images; + this.settings.auto_add_item_to_cart = profile.auto_add_item_to_cart; + this.make_app(); }); - - this.item_stock_map = {}; - - this.make_app(); } set_opening_entry_status() { @@ -238,12 +238,11 @@ erpnext.PointOfSale.Controller = class { this.item_selector = new erpnext.PointOfSale.ItemSelector({ wrapper: this.$components_wrapper, pos_profile: this.pos_profile, + settings: this.settings, events: { item_selected: args => this.on_cart_update(args), - get_frm: () => this.frm || {}, - - get_allowed_item_group: () => this.item_groups + get_frm: () => this.frm || {} } }) } @@ -251,6 +250,7 @@ erpnext.PointOfSale.Controller = class { init_item_cart() { this.cart = new erpnext.PointOfSale.ItemCart({ wrapper: this.$components_wrapper, + settings: this.settings, events: { get_frm: () => this.frm, @@ -273,9 +273,7 @@ erpnext.PointOfSale.Controller = class { this.customer_details = details; // will add/remove LP payment method this.payment.render_loyalty_points_payment_mode(); - }, - - get_allowed_customer_group: () => this.customer_groups + } } }) } diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7799dacacb..3938300a2a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1,8 +1,10 @@ erpnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events, settings }) { this.wrapper = wrapper; this.events = events; this.customer_info = undefined; + this.hide_images = settings.hide_images; + this.allowed_customer_groups = settings.customer_groups; this.init_component(); } @@ -32,6 +34,7 @@ erpnext.PointOfSale.ItemCart = class { `
    ` ) this.$customer_section = this.$component.find('.customer-section'); + this.make_customer_selector(); } reset_customer_selector() { @@ -302,7 +305,7 @@ erpnext.PointOfSale.ItemCart = class { this.$customer_section.html(`
    `); const me = this; const query = { query: 'erpnext.controllers.queries.customer_query' }; - const allowed_customer_group = this.events.get_allowed_customer_group() || []; + const allowed_customer_group = this.allowed_customer_groups || []; if (allowed_customer_group.length) { query.filters = { customer_group: ['in', allowed_customer_group] @@ -423,6 +426,7 @@ erpnext.PointOfSale.ItemCart = class { } update_customer_section() { + const me = this; const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; if (customer) { @@ -460,7 +464,7 @@ erpnext.PointOfSale.ItemCart = class { } function get_customer_image() { - if (image) { + if (!me.hide_images && image) { return `
    ${image}
    ` diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 49d42814ab..a06b3942f7 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,8 +1,10 @@ erpnext.PointOfSale.ItemSelector = class { - constructor({ frm, wrapper, events, pos_profile }) { + constructor({ frm, wrapper, events, pos_profile, settings }) { this.wrapper = wrapper; this.events = events; this.pos_profile = pos_profile; + this.hide_images = settings.hide_images; + this.auto_add_item = settings.auto_add_item_to_cart; this.inti_component(); } @@ -26,13 +28,14 @@ erpnext.PointOfSale.ItemSelector = class {
    ALL ITEMS
    -
    +
    ` ); this.$component = this.wrapper.find('.items-selector'); + this.$items_container = this.$component.find('.items-container'); } async load_items_data() { @@ -65,7 +68,6 @@ erpnext.PointOfSale.ItemSelector = class { render_item_list(items) { - this.$items_container = this.$component.find('.items-container'); this.$items_container.html(''); items.forEach(item => { @@ -75,11 +77,12 @@ erpnext.PointOfSale.ItemSelector = class { } get_item_html(item) { + const me = this; const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; function get_item_image_html() { - if (item_image) { + if (!me.hide_images && item_image) { return `
    ${frappe.get_abbr(item.item_name)}
    ` @@ -203,6 +206,7 @@ erpnext.PointOfSale.ItemSelector = class { ignore_inputs: true, page: cur_page.page.page }); + // for selecting the last filtered item on search frappe.ui.keys.on("enter", () => { const selector_is_visible = this.$component.is(':visible'); @@ -235,6 +239,7 @@ erpnext.PointOfSale.ItemSelector = class { const items = this.search_index[search_term]; this.items = items; this.render_item_list(items); + this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); return; } } @@ -247,8 +252,13 @@ erpnext.PointOfSale.ItemSelector = class { } this.items = items; this.render_item_list(items); + this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); }); } + + add_filtered_item_to_cart() { + this.$items_container.find(".item-wrapper").click(); + } resize_selector(minimize) { minimize ? diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index c716aa96e0..8473276001 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -10,8 +10,8 @@ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): filters = frappe._dict(filters or {}) if filters.from_date > filters.to_date: - frappe.throw(_('From Date cannot be greater than To Date')) - + frappe.throw(_("From Date cannot be greater than To Date")) + columns = get_columns(filters) data = get_data(filters) @@ -148,14 +148,16 @@ def get_data(filters): company_list.append(filters.get("company")) customer_details = get_customer_details() + item_details = get_item_details() sales_order_records = get_sales_order_details(company_list, filters) for record in sales_order_records: customer_record = customer_details.get(record.customer) + item_record = item_details.get(record.item_code) row = { "item_code": record.item_code, - "item_name": record.item_name, - "item_group": record.item_group, + "item_name": item_record.item_name, + "item_group": item_record.item_group, "description": record.description, "quantity": record.qty, "uom": record.uom, @@ -196,8 +198,8 @@ def get_conditions(filters): return conditions def get_customer_details(): - details = frappe.get_all('Customer', - fields=['name', 'customer_name', "customer_group"]) + details = frappe.get_all("Customer", + fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: customer_details.setdefault(d.name, frappe._dict({ @@ -206,15 +208,25 @@ def get_customer_details(): })) return customer_details +def get_item_details(): + details = frappe.db.get_all("Item", + fields=["item_code", "item_name", "item_group"]) + item_details = {} + for d in details: + item_details.setdefault(d.item_code, frappe._dict({ + "item_name": d.item_name, + "item_group": d.item_group + })) + return item_details + def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) return frappe.db.sql(""" SELECT - so_item.item_code, so_item.item_name, so_item.item_group, - so_item.description, so_item.qty, so_item.uom, - so_item.base_rate, so_item.base_amount, so.name, - so.transaction_date, so.customer, so.territory, + so_item.item_code, so_item.description, so_item.qty, + so_item.uom, so_item.base_rate, so_item.base_amount, + so.name, so.transaction_date, so.customer,so.territory, so.project, so_item.delivered_qty, so_item.billed_amt, so.company FROM diff --git a/erpnext/setup/desk_page/home/home.json b/erpnext/setup/desk_page/home/home.json index 9cf9f41907..0fbd0eccda 100644 --- a/erpnext/setup/desk_page/home/home.json +++ b/erpnext/setup/desk_page/home/home.json @@ -1,25 +1,5 @@ { "cards": [ - { - "hidden": 0, - "label": "Healthcare", - "links": "[\n {\n \"label\": \"Patient\",\n \"name\": \"Patient\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Diagnosis\",\n \"name\": \"Diagnosis\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Agriculture", - "links": "[\n {\n \"label\": \"Crop\",\n \"name\": \"Crop\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Crop Cycle\",\n \"name\": \"Crop Cycle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Location\",\n \"name\": \"Location\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fertilizer\",\n \"name\": \"Fertilizer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Education", - "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Non Profit", - "links": "[\n {\n \"description\": \"Member information.\",\n \"label\": \"Member\",\n \"name\": \"Member\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Volunteer information.\",\n \"label\": \"Volunteer\",\n \"name\": \"Volunteer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Chapter information.\",\n \"label\": \"Chapter\",\n \"name\": \"Chapter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Donor information.\",\n \"label\": \"Donor\",\n \"name\": \"Donor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, { "hidden": 0, "label": "Stock", @@ -54,10 +34,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Home", - "modified": "2020-05-11 10:20:37.358701", + "modified": "2020-12-07 14:22:38.667767", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -96,4 +77,4 @@ "type": "Page" } ] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index cbf67b4cd6..36033d9dae 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -274,7 +274,8 @@ erpnext.company.setup_queries = function(frm) { ["default_employee_advance_account", {"root_type": "Asset"}], ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], - ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}] + ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], + ["unrealized_profit_loss_account", {"root_type": "Liability"}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 40938ea0a5..d49ae7ce8a 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -46,10 +46,9 @@ "round_off_account", "round_off_cost_center", "write_off_account", - "discount_allowed_account", - "discount_received_account", "exchange_gain_loss_account", "unrealized_exchange_gain_loss_account", + "unrealized_profit_loss_account", "column_break0", "allow_account_creation_against_child_company", "default_payable_account", @@ -261,14 +260,14 @@ { "fieldname": "create_chart_of_accounts_based_on", "fieldtype": "Select", - "label": "Create Chart of Accounts Based on", + "label": "Create Chart Of Accounts Based On", "options": "\nStandard Template\nExisting Company" }, { "depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"", "fieldname": "chart_of_accounts", "fieldtype": "Select", - "label": "Chart of Accounts Template", + "label": "Chart Of Accounts Template", "no_copy": 1 }, { @@ -345,18 +344,6 @@ "label": "Write Off Account", "options": "Account" }, - { - "fieldname": "discount_allowed_account", - "fieldtype": "Link", - "label": "Discount Allowed Account", - "options": "Account" - }, - { - "fieldname": "discount_received_account", - "fieldtype": "Link", - "label": "Discount Received Account", - "options": "Account" - }, { "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", @@ -740,6 +727,12 @@ "fieldtype": "Link", "label": "Default In Transit Warehouse", "options": "Warehouse" + }, + { + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -747,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-08-06 00:38:08.311216", + "modified": "2020-12-03 12:27:27.085094", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -808,4 +801,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3..9068e338c3 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-12-02 15:47:41.532942", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ccc719c26d..18665577e8 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -157,6 +157,11 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } if (!doc.is_return && doc.status!="Closed") { + if(doc.docstatus == 1) { + this.frm.add_custom_button(__('Shipment'), function() { + me.make_shipment() }, __('Create')); + } + if(flt(doc.per_installed, 2) < 100 && doc.docstatus==1) this.frm.add_custom_button(__('Installation Note'), function() { me.make_installation_note() }, __('Create')); @@ -221,6 +226,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }, + make_shipment: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_shipment", + frm: this.frm + }) + }, + make_sales_invoice: function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 7393c8a70e..c9f8d0810e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -133,6 +133,7 @@ "per_installed", "installation_status", "column_break_89", + "per_returned", "excise_page", "instructions", "subscription_section", @@ -1099,7 +1100,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed", + "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1251,13 +1252,22 @@ "fieldtype": "Link", "label": "Inter Company Reference", "options": "Purchase Receipt" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_returned", + "fieldtype": "Percent", + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-11-11 14:57:16.388139", + "modified": "2020-11-30 12:54:45.407289", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d04cf785ab..3f3407e350 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -55,7 +55,7 @@ class DeliveryNote(SellingController): 'no_allowance': 1 }] if cint(self.is_return): - self.status_updater.append({ + self.status_updater.extend([{ 'source_dt': 'Delivery Note Item', 'target_dt': 'Sales Order Item', 'join_field': 'so_detail', @@ -69,7 +69,19 @@ class DeliveryNote(SellingController): where name=`tabDelivery Note Item`.parent and is_return=1)""", 'second_source_extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""" - }) + }, + { + 'source_dt': 'Delivery Note Item', + 'target_dt': 'Delivery Note Item', + 'join_field': 'dn_detail', + 'target_field': 'returned_qty', + 'target_parent_dt': 'Delivery Note', + 'target_parent_field': 'per_returned', + 'target_ref_field': 'stock_qty', + 'source_field': '-1 * stock_qty', + 'percent_join_field_parent': 'return_against' + } + ]) def before_print(self): def toggle_print_hide(meta, fieldname): @@ -569,6 +581,62 @@ def make_packing_slip(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_shipment(source_name, target_doc=None): + def postprocess(source, target): + user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + target.pickup_contact_email = user.email + pickup_contact_display = '{}'.format(user.full_name) + if user: + if user.email: + pickup_contact_display += '
    ' + user.email + if user.phone: + pickup_contact_display += '
    ' + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += '
    ' + user.mobile_no + target.pickup_contact = pickup_contact_display + + contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) + delivery_contact_display = '{}'.format(source.contact_display) + if contact: + if contact.email_id: + delivery_contact_display += '
    ' + contact.email_id + if contact.phone: + delivery_contact_display += '
    ' + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += '
    ' + contact.mobile_no + target.delivery_contact = delivery_contact_display + + doclist = get_mapped_doc("Delivery Note", source_name, { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "address_display": "delivery_address", + "customer": "delivery_customer", + "shipping_address_name": "delivery_address_name", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email" + }, + "validation": { + "docstatus": ["=", 1] + } + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total" + } + } + }, target_doc, postprocess) + + return doclist @frappe.whitelist() def make_sales_return(source_name, target_doc=None): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 0ae7c37b3f..4a6500cfd8 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -6,9 +6,11 @@ frappe.listview_settings['Delivery Note'] = { return [__("Return"), "darkgrey", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; + } else if (flt(doc.per_returned, 2) === 100) { + return [__("Return Issued"), "grey", "per_returned,=,100"]; } else if (flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; - } else if (flt(doc.per_billed, 2) == 100) { + } else if (flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } }, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9566af7b38..6b4663a688 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -206,7 +206,7 @@ class TestDeliveryNote(unittest.TestCase): for field, value in field_values.items(): self.assertEqual(cstr(serial_no.get(field)), value) - def test_sales_return_for_non_bundled_items(self): + def test_sales_return_for_non_bundled_items_partial(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) @@ -225,7 +225,10 @@ class TestDeliveryNote(unittest.TestCase): # return entry dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", do_not_submit=1) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1") @@ -243,6 +246,70 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(gle_warehouse_amount, stock_value_difference) + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Delivery Note", dn1.name) + returned.update_prevdoc_status() + dn.load_from_db() + + # Check if Original DN updated + self.assertEqual(dn.items[0].returned_qty, 2) + self.assertEqual(dn.per_returned, 40) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_dn_2 = make_return_doc("Delivery Note", dn.name) + + # Check if unreturned amount is mapped in 2nd return + self.assertEqual(return_dn_2.items[0].qty, -3) + + si = make_sales_invoice(dn.name) + si.submit() + + self.assertEqual(si.items[0].qty, 3) + + dn.load_from_db() + # DN should be completed on billing all unreturned amount + self.assertEqual(dn.items[0].billed_amt, 1500) + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.status, 'Completed') + + si.load_from_db() + si.cancel() + + dn.load_from_db() + self.assertEqual(dn.per_billed, 0) + + dn1.cancel() + dn.cancel() + + def test_sales_return_for_non_bundled_items_full(self): + from erpnext.stock.doctype.item.test_item import make_item + + company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + + make_item("Box", {'is_stock_item': 1}) + + make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100) + + dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company, + expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + + #return entry + dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500, + company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", do_not_submit=1) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() + + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Delivery Note", dn1.name) + returned.update_prevdoc_status() + dn.load_from_db() + + # Check if Original DN updated + self.assertEqual(dn.items[0].returned_qty, 5) + self.assertEqual(dn.per_returned, 100) + self.assertEqual(dn.status, 'Return Issued') + def test_return_single_item_from_bundled_items(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 3d57f47601..7b471874af 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-04-22 13:15:44", "doctype": "DocType", @@ -24,7 +25,10 @@ "col_break2", "uom", "conversion_factor", + "stock_qty_sec_break", "stock_qty", + "stock_qty_col_break", + "returned_qty", "section_break_17", "price_list_rate", "base_price_list_rate", @@ -211,7 +215,7 @@ { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Qty as per Stock UOM", + "label": "Qty in Stock UOM", "no_copy": 1, "print_hide": 1, "read_only": 1 @@ -715,12 +719,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "stock_qty_sec_break", + "fieldtype": "Section Break" + }, + { + "fieldname": "stock_qty_col_break", + "fieldtype": "Column Break" + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty in Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-07-20 12:25:06.177894", + "modified": "2020-07-31 20:12:43.054342", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 13c8ceb759..5bb3095708 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -111,6 +111,7 @@ "range", "column_break4", "per_billed", + "per_returned", "is_internal_supplier", "inter_company_reference", "subscription_detail", @@ -895,7 +896,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed", + "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1104,13 +1105,22 @@ "fieldtype": "Small Text", "label": "Billing Address", "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_returned", + "fieldtype": "Percent", + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-10-30 14:00:08.347534", + "modified": "2020-11-30 12:54:23.278500", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 2cc4679c8c..97e0fa738c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -55,20 +55,33 @@ class PurchaseReceipt(BuyingController): 'percent_join_field': 'material_request' }] if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'po_detail', - 'extra_cond': """ and exists (select name from `tabPurchase Receipt` - where name=`tabPurchase Receipt Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" - }) + self.status_updater.extend([ + { + 'source_dt': 'Purchase Receipt Item', + 'target_dt': 'Purchase Order Item', + 'join_field': 'purchase_order_item', + 'target_field': 'returned_qty', + 'source_field': '-1 * qty', + 'second_source_dt': 'Purchase Invoice Item', + 'second_source_field': '-1 * qty', + 'second_join_field': 'po_detail', + 'extra_cond': """ and exists (select name from `tabPurchase Receipt` + where name=`tabPurchase Receipt Item`.parent and is_return=1)""", + 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" + }, + { + 'source_dt': 'Purchase Receipt Item', + 'target_dt': 'Purchase Receipt Item', + 'join_field': 'purchase_receipt_item', + 'target_field': 'returned_qty', + 'target_parent_dt': 'Purchase Receipt', + 'target_parent_field': 'per_returned', + 'target_ref_field': 'received_stock_qty', + 'source_field': '-1 * received_stock_qty', + 'percent_join_field_parent': 'return_against' + } + ]) def validate(self): self.validate_posting_time() @@ -478,7 +491,7 @@ class PurchaseReceipt(BuyingController): frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate)) def update_status(self, status): - self.set_status(update=True, status = status) + self.set_status(update=True, status=status) self.notify_update() clear_doctype_notifications(self) @@ -490,7 +503,7 @@ class PurchaseReceipt(BuyingController): for pr in set(updated_pr): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) - pr_doc.update_billing_percentage(update_modified=update_modified) + update_billing_percentage(pr_doc, update_modified=update_modified) self.load_from_db() @@ -500,7 +513,7 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail) billed_against_po = billed_against_po and billed_against_po[0][0] or 0 - # Get all Delivery Note Item rows against the Sales Order Item row + # Get all Purchase Receipt Item rows against the Purchase Order Item row pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name=pr_item.parent and pr_item.purchase_order_item=%s @@ -530,6 +543,39 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): return updated_pr +def update_billing_percentage(pr_doc, update_modified=True): + # Reload as billed amount was set in db directly + pr_doc.load_from_db() + + # Update Billing % based on pending accepted qty + total_amount, total_billed_amount = 0, 0 + for item in pr_doc.items: + return_data = frappe.db.get_list("Purchase Receipt", + fields = [ + "sum(abs(`tabPurchase Receipt Item`.qty)) as qty" + ], + filters = [ + ["Purchase Receipt", "docstatus", "=", 1], + ["Purchase Receipt", "is_return", "=", 1], + ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name] + ]) + + returned_qty = return_data[0].qty if return_data else 0 + returned_amount = flt(returned_qty) * flt(item.rate) + pending_amount = flt(item.amount) - returned_amount + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + + total_amount += total_billable_amount + total_billed_amount += flt(item.billed_amt) + + percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) + pr_doc.db_set("per_billed", percent_billed) + pr_doc.load_from_db() + + if update_modified: + pr_doc.set_status(update=True) + pr_doc.notify_update() + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc @@ -552,6 +598,7 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) + target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index e81f323a46..c9501a409a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -6,9 +6,11 @@ frappe.listview_settings['Purchase Receipt'] = { return [__("Return"), "darkgrey", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; + } else if (flt(doc.per_returned, 2) === 100) { + return [__("Return Issued"), "grey", "per_returned,=,100"]; } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; - } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) == 100) { + } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 253edb02c3..9b8eeed1a1 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -137,7 +137,10 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) def test_purchase_receipt_gl_entry(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", + get_multiple_items = True, get_taxes_and_charges = True) + self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -281,11 +284,15 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse) - def test_purchase_return(self): + def test_purchase_return_partial(self): + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") - - return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2) + return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", + is_return=1, return_against=pr.name, qty=-2, do_not_submit=1) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() # check sle outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", @@ -309,6 +316,60 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_pr_2 = make_return_doc("Purchase Receipt", pr.name) + + # Check if unreturned amount is mapped in 2nd return + self.assertEqual(return_pr_2.items[0].qty, -3) + + # Make PI against unreturned amount + pi = make_purchase_invoice(pr.name) + pi.submit() + + self.assertEqual(pi.items[0].qty, 3) + + pr.load_from_db() + # PR should be completed on billing all unreturned amount + self.assertEqual(pr.items[0].billed_amt, 150) + self.assertEqual(pr.per_billed, 100) + self.assertEqual(pr.status, 'Completed') + + pi.load_from_db() + pi.cancel() + + pr.load_from_db() + self.assertEqual(pr.per_billed, 0) + + return_pr.cancel() + pr.cancel() + + def test_purchase_return_full(self): + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1") + + return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 5) + self.assertEqual(pr.per_returned, 100) + self.assertEqual(pr.status, 'Return Issued') def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse @@ -416,6 +477,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr1.per_billed, 100) self.assertEqual(pr1.status, "Completed") + pr2.load_from_db() self.assertEqual(pr2.get("items")[0].billed_amt, 2000) self.assertEqual(pr2.per_billed, 80) self.assertEqual(pr2.status, "To Bill") diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index c1e1f901ba..84c64aa8f8 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -28,9 +28,13 @@ "uom", "stock_uom", "conversion_factor", - "stock_qty", "retain_sample", "sample_quantity", + "tracking_section", + "received_stock_qty", + "stock_qty", + "col_break_tracking_section", + "returned_qty", "rate_and_amount", "price_list_rate", "discount_percentage", @@ -526,7 +530,7 @@ { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Accepted Qty as per Stock UOM", + "label": "Accepted Qty in Stock UOM", "oldfieldname": "stock_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -834,12 +838,35 @@ "collapsible": 1, "fieldname": "image_column", "fieldtype": "Column Break" + }, + { + "fieldname": "tracking_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_tracking_section", + "fieldtype": "Column Break" + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty in Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "received_stock_qty", + "fieldtype": "Float", + "label": "Received Qty in Stock UOM", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-28 19:01:21.154963", + "modified": "2020-11-02 10:00:38.204294", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/shipment/__init__.py b/erpnext/stock/doctype/shipment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js new file mode 100644 index 0000000000..5ccb7d2ff6 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -0,0 +1,447 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment', { + address_query: function(frm, link_doctype, link_name, is_your_company_address) { + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: link_doctype, + link_name: link_name, + is_your_company_address: is_your_company_address + } + }; + }, + contact_query: function(frm, link_doctype, link_name) { + return { + query: 'frappe.contacts.doctype.contact.contact.contact_query', + filters: { + link_doctype: link_doctype, + link_name: link_name + } + }; + }, + onload: function(frm) { + frm.set_query("delivery_address_name", () => { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0); + }); + frm.set_query("pickup_address_name", () => { + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0); + }); + frm.set_query("delivery_contact_name", () => { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + return frm.events.contact_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to]); + }); + frm.set_query("pickup_contact_name", () => { + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]); + }); + frm.set_query("delivery_note", "shipment_delivery_note", function() { + let customer = ''; + if (frm.doc.delivery_to_type == "Customer") { + customer = frm.doc.delivery_customer; + } + if (frm.doc.delivery_to_type == "Company") { + customer = frm.doc.delivery_company; + } + if (customer) { + return { + filters: { + customer: customer, + docstatus: 1, + status: ["not in", ["Cancelled"]] + } + }; + } + }); + }, + refresh: function() { + $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); + $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); + }, + before_save: function(frm) { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + frm.set_value("delivery_to", frm.doc[delivery_to]); + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + frm.set_value("pickup", frm.doc[pickup_from]); + }, + set_pickup_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.pickup_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("pickup_address_name", r.name); + }); + }, + set_delivery_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.delivery_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("delivery_address_name", r.name); + }); + }, + pickup_from_type: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + frm.set_value("pickup_company", frappe.defaults.get_default('company')); + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_supplier", ''); + } else { + frm.trigger('clear_pickup_fields'); + } + if (frm.doc.pickup_from_type == 'Customer') { + frm.set_value("pickup_company", ''); + frm.set_value("pickup_supplier", ''); + } + if (frm.doc.pickup_from_type == 'Supplier') { + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_company", ''); + } + }, + delivery_to_type: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + frm.set_value("delivery_company", frappe.defaults.get_default('company')); + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_supplier", ''); + } else { + frm.trigger('clear_delivery_fields'); + } + if (frm.doc.delivery_to_type == 'Customer') { + frm.set_value("delivery_company", ''); + frm.set_value("delivery_supplier", ''); + } + if (frm.doc.delivery_to_type == 'Supplier') { + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_company", ''); + frm.toggle_display("shipment_delivery_note", false); + } else { + frm.toggle_display("shipment_delivery_note", true); + } + }, + delivery_address_name: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true); + } else { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false); + } + }, + pickup_address_name: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true); + } else { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false); + } + }, + get_contact_display: function(frm, contact_name, contact_type) { + frappe.call({ + method: "frappe.contacts.doctype.contact.contact.get_contact_details", + args: { contact: contact_name }, + callback: function(r) { + if (r.message) { + if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { + if (contact_type == 'Delivery') { + frm.set_value('delivery_contact_name', ''); + frm.set_value('delivery_contact', ''); + } else { + frm.set_value('pickup_contact_name', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") + "
    " + __("Please set Email/Phone for the contact") + ` ${contact_name}`); + } + let contact_display = r.message.contact_display; + if (r.message.contact_email) { + contact_display += '
    ' + r.message.contact_email; + } + if (r.message.contact_phone) { + contact_display += '
    ' + r.message.contact_phone; + } + if (r.message.contact_mobile && !r.message.contact_phone) { + contact_display += '
    ' + r.message.contact_mobile; + } + if (contact_type == 'Delivery') { + frm.set_value('delivery_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('delivery_contact_email', r.message.contact_email); + } + } else { + frm.set_value('pickup_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('pickup_contact_email', r.message.contact_email); + } + } + } + } + }); + }, + delivery_contact_name: function(frm) { + if (frm.doc.delivery_contact_name) { + frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery'); + } + }, + pickup_contact_name: function(frm) { + if (frm.doc.pickup_contact_name) { + frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup'); + } + }, + pickup_contact_person: function(frm) { + if (frm.doc.pickup_contact_person) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frm.doc.pickup_contact_person }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
    ${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
    ${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
    ${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } else { + if (frm.doc.pickup_from_type === 'Company') { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frappe.session.user }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
    ${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
    ${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
    ${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } + } + }, + set_company_contact: function(frm, delivery_type) { + frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => { + if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_company', ''); + frm.set_value('delivery_contact', ''); + } else { + frm.set_value('pickup_company', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
    " + __("Please first set Last Name, Email and Phone for the user") + ` ${frappe.session.user}`); + } + let contact_display = r.full_name; + if (r.email) { + contact_display += '
    ' + r.email; + } + if (r.phone) { + contact_display += '
    ' + r.phone; + } + if (r.mobile_no && !r.phone) { + contact_display += '
    ' + r.mobile_no; + } + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact', contact_display); + if (r.email) { + frm.set_value('delivery_contact_email', r.email); + } + } else { + frm.set_value('pickup_contact', contact_display); + if (r.email) { + frm.set_value('pickup_contact_email', r.email); + } + } + }); + frm.set_value('pickup_contact_person', frappe.session.user); + }, + pickup_company: function(frm) { + if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) { + frm.trigger('set_pickup_company_address'); + frm.events.set_company_contact(frm, 'Pickup'); + } + }, + delivery_company: function(frm) { + if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) { + frm.trigger('set_delivery_company_address'); + frm.events.set_company_contact(frm, 'Delivery'); + } + }, + delivery_customer: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_customer) { + frm.events.set_address_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + } + }, + delivery_supplier: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_supplier) { + frm.events.set_address_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + } + }, + pickup_customer: function(frm) { + if (frm.doc.pickup_customer) { + frm.events.set_address_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + } + }, + pickup_supplier: function(frm) { + if (frm.doc.pickup_supplier) { + frm.events.set_address_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + } + }, + set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_address_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if (r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_address_name', r.message); + } else { + frm.set_value('pickup_address_name', r.message); + } + } + } + }); + }, + set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_contact_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if (r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact_name', r.message); + } else { + frm.set_value('pickup_contact_name', r.message); + } + } + } + }); + }, + add_template: function(frm) { + if (frm.doc.parcel_template) { + frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => { + let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template); + let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel"); + row.length = parcel_template.length; + row.width = parcel_template.width; + row.height = parcel_template.height; + row.weight = parcel_template.weight; + frm.refresh_fields("shipment_parcel"); + }); + } + }, + pickup_date: function(frm) { + if (frm.doc.pickup_date < frappe.datetime.get_today()) { + frappe.throw(__("Pickup Date cannot be before this day")); + } + if (frm.doc.pickup_date == frappe.datetime.get_today()) { + var pickup_time = frm.events.get_pickup_time(frm); + frm.set_value("pickup_from", pickup_time); + frm.trigger('set_pickup_to_time'); + } + }, + pickup_from: function(frm) { + var pickup_time = frm.events.get_pickup_time(frm); + if (frm.doc.pickup_from && frm.doc.pickup_date == frappe.datetime.get_today()) { + let current_hour = pickup_time.split(':')[0]; + let current_min = pickup_time.split(':')[1]; + let pickup_hour = frm.doc.pickup_from.split(':')[0]; + let pickup_min = frm.doc.pickup_from.split(':')[1]; + if (pickup_hour < current_hour || (pickup_hour == current_hour && pickup_min < current_min)) { + frm.set_value("pickup_from", pickup_time); + frappe.throw(__("Pickup Time cannot be in the past")); + } + } + frm.trigger('set_pickup_to_time'); + }, + get_pickup_time: function() { + let current_hour = new Date().getHours(); + let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'}); + if (current_min < 30) { + current_min = '30'; + } else { + current_min = '00'; + current_hour = Number(current_hour)+1; + } + let pickup_time = current_hour +':'+ current_min; + return pickup_time; + }, + set_pickup_to_time: function(frm) { + let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5; + let pickup_to_min = frm.doc.pickup_from.split(':')[1]; + let pickup_to = pickup_to_hour +':'+ pickup_to_min; + frm.set_value("pickup_to", pickup_to); + }, + clear_pickup_fields: function(frm) { + let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; + for (let field of fields) { + frm.set_value(field, ''); + } + }, + clear_delivery_fields: function(frm) { + let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"]; + for (let field of fields) { + frm.set_value(field, ''); + } + }, + remove_email_row: function(frm, table, fieldname) { + $.each(frm.doc[table] || [], function(i, detail) { + if (detail.email === fieldname) { + cur_frm.get_field(table).grid.grid_rows[i].remove(); + } + }); + } +}); + +frappe.ui.form.on('Shipment Delivery Note', { + delivery_note: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.delivery_note) { + let row_index = row.idx - 1; + if (validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { + frappe.throw(__("You have entered a duplicate Delivery Note on Row") + ` ${row.idx}. ` + __("Please rectify and try again.")); + } + } + }, + grand_total: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.grand_total) { + var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total); + frm.set_value("value_of_goods", Math.round(value_of_goods)); + frm.refresh_fields("value_of_goods"); + } + }, +}); + +var validate_duplicate = function(frm, table, fieldname, index) { + return ( + table === 'shipment_delivery_note' + ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) + : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)) + ); +}; diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json new file mode 100644 index 0000000000..37a9cc6c02 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -0,0 +1,471 @@ +{ + "actions": [], + "autoname": "SHIPMENT-.#####", + "creation": "2020-07-09 10:58:52.508703", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "heading_pickup_from", + "pickup_from_type", + "pickup_company", + "pickup_customer", + "pickup_supplier", + "pickup", + "pickup_address_name", + "pickup_address", + "pickup_contact_person", + "pickup_contact_name", + "pickup_contact_email", + "pickup_contact", + "column_break_2", + "heading_delivery_to", + "delivery_to_type", + "delivery_company", + "delivery_customer", + "delivery_supplier", + "delivery_to", + "delivery_address_name", + "delivery_address", + "delivery_contact_name", + "delivery_contact_email", + "delivery_contact", + "parcels_section", + "shipment_parcel", + "parcel_template", + "add_template", + "column_break_28", + "shipment_delivery_note", + "shipment_details_section", + "pallets", + "value_of_goods", + "pickup_date", + "pickup_from", + "pickup_to", + "column_break_36", + "shipment_type", + "pickup_type", + "incoterm", + "description_of_content", + "section_break_40", + "shipment_information_section", + "service_provider", + "shipment_id", + "shipment_amount", + "status", + "tracking_url", + "column_break_55", + "carrier", + "carrier_service", + "awb_number", + "tracking_status", + "tracking_status_info", + "amended_from" + ], + "fields": [ + { + "fieldname": "heading_pickup_from", + "fieldtype": "Heading", + "label": "Pickup from" + }, + { + "default": "Company", + "fieldname": "pickup_from_type", + "fieldtype": "Select", + "label": "Pickup from", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Company'", + "fieldname": "pickup_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Customer'", + "fieldname": "pickup_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Supplier'", + "fieldname": "pickup_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "pickup", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Pickup From", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"", + "fieldname": "pickup_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "pickup_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type !== \"Company\"", + "fieldname": "pickup_contact_name", + "fieldtype": "Link", + "label": "Contact", + "mandatory_depends_on": "eval: doc.pickup_from_type !== 'Company'", + "options": "Contact" + }, + { + "fieldname": "pickup_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "fieldname": "pickup_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "heading_delivery_to", + "fieldtype": "Heading", + "label": "Delivery to" + }, + { + "default": "Customer", + "fieldname": "delivery_to_type", + "fieldtype": "Select", + "label": "Delivery to", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Company'", + "fieldname": "delivery_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Customer'", + "fieldname": "delivery_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Supplier'", + "fieldname": "delivery_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "delivery_to", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Delivery To", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "delivery_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_contact_name", + "fieldtype": "Link", + "label": "Contact", + "mandatory_depends_on": "eval: doc.delivery_from_type !== 'Company'", + "options": "Contact" + }, + { + "fieldname": "delivery_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "depends_on": "eval:doc.delivery_contact_name", + "fieldname": "delivery_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "parcels_section", + "fieldtype": "Section Break", + "label": "Parcels" + }, + { + "fieldname": "shipment_parcel", + "fieldtype": "Table", + "label": "Shipment Parcel", + "options": "Shipment Parcel" + }, + { + "fieldname": "parcel_template", + "fieldtype": "Link", + "label": "Parcel Template", + "options": "Shipment Parcel Template" + }, + { + "depends_on": "eval:doc.docstatus !== 1\n", + "fieldname": "add_template", + "fieldtype": "Button", + "label": "Add Template" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipment_details_section", + "fieldtype": "Section Break", + "label": "Shipment details" + }, + { + "default": "No", + "fieldname": "pallets", + "fieldtype": "Select", + "label": "Pallets", + "options": "No\nYes" + }, + { + "fieldname": "value_of_goods", + "fieldtype": "Currency", + "label": "Value of Goods", + "precision": "2", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "pickup_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Pickup Date", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "09:00", + "fieldname": "pickup_from", + "fieldtype": "Time", + "label": "Pickup from" + }, + { + "allow_on_submit": 1, + "default": "17:00", + "fieldname": "pickup_to", + "fieldtype": "Time", + "label": "Pickup to" + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "default": "Goods", + "fieldname": "shipment_type", + "fieldtype": "Select", + "label": "Shipment Type", + "options": "Goods\nDocuments" + }, + { + "default": "Pickup", + "fieldname": "pickup_type", + "fieldtype": "Select", + "label": "Pickup Type", + "options": "Pickup\nSelf delivery" + }, + { + "fieldname": "description_of_content", + "fieldtype": "Small Text", + "label": "Description of Content", + "reqd": 1 + }, + { + "fieldname": "section_break_40", + "fieldtype": "Section Break" + }, + { + "fieldname": "shipment_information_section", + "fieldtype": "Section Break", + "label": "Shipment Information" + }, + { + "fieldname": "service_provider", + "fieldtype": "Data", + "label": "Service Provider", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "shipment_id", + "fieldtype": "Data", + "label": "Shipment ID", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "shipment_amount", + "fieldtype": "Currency", + "label": "Shipment Amount", + "no_copy": 1, + "precision": "2", + "print_hide": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", + "print_hide": 1 + }, + { + "fieldname": "tracking_url", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Tracking URL", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "carrier", + "fieldtype": "Data", + "label": "Carrier", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "carrier_service", + "fieldtype": "Data", + "label": "Carrier Service", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "awb_number", + "fieldtype": "Data", + "label": "AWB Number", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "tracking_status", + "fieldtype": "Select", + "label": "Tracking Status", + "no_copy": 1, + "options": "\nIn Progress\nDelivered\nReturned\nLost", + "print_hide": 1 + }, + { + "fieldname": "tracking_status_info", + "fieldtype": "Data", + "label": "Tracking Status Info", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Shipment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Select", + "label": "Incoterm", + "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)" + }, + { + "fieldname": "shipment_delivery_note", + "fieldtype": "Table", + "label": "Shipment Delivery Note", + "options": "Shipment Delivery Note" + }, + { + "depends_on": "eval:doc.pickup_from_type === 'Company'", + "fieldname": "pickup_contact_person", + "fieldtype": "Link", + "label": "Pickup Contact Person", + "mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'", + "options": "User" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-12-02 15:43:44.607039", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py new file mode 100644 index 0000000000..de0c243b05 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt +from frappe.model.document import Document +from erpnext.accounts.party import get_party_shipping_address +from frappe.contacts.doctype.contact.contact import get_default_contact + +class Shipment(Document): + def validate(self): + self.validate_weight() + self.set_value_of_goods() + if self.docstatus == 0: + self.status = 'Draft' + + def on_submit(self): + if not self.shipment_parcel: + frappe.throw(_('Please enter Shipment Parcel information')) + if self.value_of_goods == 0: + frappe.throw(_('Value of goods cannot be 0')) + self.status = 'Submitted' + + def on_cancel(self): + self.status = 'Cancelled' + + def validate_weight(self): + for parcel in self.shipment_parcel: + if flt(parcel.weight) <= 0: + frappe.throw(_('Parcel weight cannot be 0')) + + def set_value_of_goods(self): + value_of_goods = 0 + for entry in self.get("shipment_delivery_note"): + value_of_goods += flt(entry.get("grand_total")) + self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + +@frappe.whitelist() +def get_address_name(ref_doctype, docname): + # Return address name + return get_party_shipping_address(ref_doctype, docname) + +@frappe.whitelist() +def get_contact_name(ref_doctype, docname): + # Return address name + return get_default_contact(ref_doctype, docname) + +@frappe.whitelist() +def get_company_contact(user): + contact = frappe.db.get_value('User', user, [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.phone: + contact.phone = contact.mobile_no + return contact diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js new file mode 100644 index 0000000000..52b052c81f --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings['Shipment'] = { + add_fields: ["status"], + get_indicator: function(doc) { + if (doc.status=='Booked') { + return [__("Booked"), "green"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py new file mode 100644 index 0000000000..e1fa207a21 --- /dev/null +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +from datetime import date, timedelta + +import frappe +import unittest +from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment + +class TestShipment(unittest.TestCase): + def test_shipment_from_delivery_note(self): + delivery_note = create_test_delivery_note() + delivery_note.submit() + shipment = create_test_shipment([ delivery_note ]) + shipment.submit() + second_shipment = make_shipment(delivery_note.name) + self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) + self.assertEqual(len(second_shipment.shipment_delivery_note), 1) + self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) + +def create_test_delivery_note(): + company = get_shipment_company() + customer = get_shipment_customer() + item = get_shipment_item(company.name) + posting_date = date.today() + timedelta(days=1) + + create_material_receipt(item, company.name) + delivery_note = frappe.new_doc("Delivery Note") + delivery_note.company = company.name + delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") + delivery_note.posting_time = '10:00' + delivery_note.customer = customer.name + delivery_note.append('items', + { + "item_code": item.name, + "item_name": item.item_name, + "description": 'Test delivery note for shipment', + "qty": 5, + "uom": 'Nos', + "warehouse": 'Stores - SC', + "rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + delivery_note.insert() + frappe.db.commit() + return delivery_note + + +def create_test_shipment(delivery_notes = None): + company = get_shipment_company() + company_address = get_shipment_company_address(company.name) + customer = get_shipment_customer() + customer_address = get_shipment_customer_address(customer.name) + customer_contact = get_shipment_customer_contact(customer.name) + posting_date = date.today() + timedelta(days=5) + + shipment = frappe.new_doc("Shipment") + shipment.pickup_from_type = 'Company' + shipment.pickup_company = company.name + shipment.pickup_address_name = company_address.name + shipment.delivery_to_type = 'Customer' + shipment.delivery_customer = customer.name + shipment.delivery_address_name = customer_address.name + shipment.delivery_contact_name = customer_contact.name + shipment.pallets = 'No' + shipment.shipment_type = 'Goods' + shipment.value_of_goods = 1000 + shipment.pickup_type = 'Pickup' + shipment.pickup_date = posting_date.strftime("%Y-%m-%d") + shipment.pickup_from = '09:00' + shipment.pickup_to = '17:00' + shipment.description_of_content = 'unit test entry' + for delivery_note in delivery_notes: + shipment.append('shipment_delivery_note', + { + "delivery_note": delivery_note.name + } + ) + shipment.append('shipment_parcel', + { + "length": 5, + "width": 5, + "height": 5, + "weight": 5, + "count": 5 + } + ) + shipment.insert() + frappe.db.commit() + return shipment + + +def get_shipment_customer_contact(customer_name): + contact_fname = 'Customer Shipment' + contact_lname = 'Testing' + customer_name = contact_fname + ' ' + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + if len(contacts): + return contacts[0] + else: + return create_customer_contact(contact_fname, contact_lname) + + +def get_shipment_customer_address(customer_name): + address_title = customer_name + ' address 123' + customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(customer_address): + return customer_address[0] + else: + return create_shipment_address(address_title, customer_name, 81929) + +def get_shipment_customer(): + customer_name = 'Shipment Customer' + customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + if len(customer): + return customer[0] + else: + return create_shipment_customer(customer_name) + +def get_shipment_company_address(company_name): + address_title = company_name + ' address 123' + addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(addresses): + return addresses[0] + else: + return create_shipment_address(address_title, company_name, 80331) + +def get_shipment_company(): + company_name = 'Shipment Company' + abbr = 'SC' + companies = frappe.get_all("Company", fields=["name"], filters = {"company_name": company_name}) + if len(companies): + return companies[0] + else: + return create_shipment_company(company_name, abbr) + +def get_shipment_item(company_name): + item_name = 'Testing Shipment item' + items = frappe.get_all("Item", + fields=["name", "item_name", "item_code", "standard_rate"], + filters = {"item_name": item_name} + ) + if len(items): + return items[0] + else: + return create_shipment_item(item_name, company_name) + +def create_shipment_address(address_title, company_name, postal_code): + address = frappe.new_doc("Address") + address.address_title = address_title + address.address_type = 'Shipping' + address.address_line1 = company_name + ' address line 1' + address.city = 'Random City' + address.postal_code = postal_code + address.country = 'Germany' + address.insert() + return address + + +def create_customer_contact(fname, lname): + customer = frappe.new_doc("Contact") + customer.customer_name = fname + ' ' + lname + customer.first_name = fname + customer.last_name = lname + customer.is_primary_contact = 1 + customer.is_billing_contact = 1 + customer.append('email_ids', + { + 'email_id': 'randomme@email.com', + 'is_primary': 1 + } + ) + customer.append('phone_nos', + { + 'phone': '123123123', + 'is_primary_phone': 1, + 'is_primary_mobile_no': 1 + } + ) + customer.status = 'Passive' + customer.insert() + return customer + + +def create_shipment_company(company_name, abbr): + company = frappe.new_doc("Company") + company.company_name = company_name + company.abbr = abbr + company.default_currency = 'EUR' + company.country = 'Germany' + company.insert() + return company + +def create_shipment_customer(customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.customer_type = 'Company' + customer.customer_group = 'All Customer Groups' + customer.territory = 'All Territories' + customer.gst_category = 'Unregistered' + customer.insert() + return customer + +def create_material_receipt(item, company): + posting_date = date.today() + stock = frappe.new_doc("Stock Entry") + stock.company = company + stock.stock_entry_type = 'Material Receipt' + stock.posting_date = posting_date.strftime("%Y-%m-%d") + stock.append('items', + { + "t_warehouse": 'Stores - SC', + "item_code": item.name, + "qty": 5, + "uom": 'Nos', + "basic_rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + stock.insert() + stock.submit() + + +def create_shipment_item(item_name, company_name): + item = frappe.new_doc("Item") + item.item_name = item_name + item.item_code = item_name + item.item_group = 'All Item Groups' + item.stock_uom = 'Nos' + item.standard_rate = 50 + item.append('item_defaults', + { + "company": company_name, + "default_warehouse": 'Stores - SC' + } + ) + item.insert() + return item diff --git a/erpnext/stock/doctype/shipment_delivery_note/__init__.py b/erpnext/stock/doctype/shipment_delivery_note/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json new file mode 100644 index 0000000000..8625913718 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-07-09 11:52:57.939021", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "delivery_note", + "grand_total" + ], + "fields": [ + { + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "options": "Delivery Note", + "reqd": 1 + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Value", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-12-02 15:44:34.028703", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Delivery Note", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py new file mode 100644 index 0000000000..4342151605 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentDeliveryNote(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel/__init__.py b/erpnext/stock/doctype/shipment_parcel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json new file mode 100644 index 0000000000..6943edcdc9 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "creation": "2020-07-09 11:28:48.887737", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "length", + "width", + "height", + "weight", + "count" + ], + "fields": [ + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Count", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:54:14.847170", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py new file mode 100644 index 0000000000..53e6ed55dd --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentParcel(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/__init__.py b/erpnext/stock/doctype/shipment_parcel_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js new file mode 100644 index 0000000000..785a3b304d --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment Parcel Template', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json new file mode 100644 index 0000000000..4735d9f886 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "autoname": "field:parcel_template_name", + "creation": "2020-07-09 11:43:43.470339", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parcel_template_name", + "length", + "width", + "height", + "weight" + ], + "fields": [ + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + }, + { + "fieldname": "parcel_template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parcel Template Name", + "reqd": 1, + "unique": 1 + } + ], + "links": [], + "modified": "2020-09-28 12:51:00.320421", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py new file mode 100644 index 0000000000..2a8d58d830 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentParcelTemplate(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py new file mode 100644 index 0000000000..6e2caa768b --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestShipmentParcelTemplate(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 80f18a63f5..ae21d9415f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -845,6 +845,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } }, + fg_completed_qty: function() { + this.get_items(); + }, + get_items: function() { var me = this; if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) @@ -854,6 +858,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ // if work order / bom is mentioned, get items return this.frm.call({ doc: me.frm.doc, + freeze: true, method: "get_items", callback: function(r) { if(!r.exc) refresh_field("items"); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3159b95c3..32d7e6eb34 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -120,6 +120,7 @@ class StockEntry(StockController): self.update_transferred_qty() self.update_quality_inspection() self.delete_auto_created_batches() + self.delete_linked_stock_entry() if self.purpose == 'Material Transfer' and self.add_to_transit: self.set_material_request_transfer_status('Not Started') @@ -152,6 +153,12 @@ class StockEntry(StockController): frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") .format(self.job_card)) + def delete_linked_stock_entry(self): + if self.purpose == "Send to Warehouse": + for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, + "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + frappe.delete_doc("Stock Entry", d.name) + def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): @@ -1033,26 +1040,22 @@ class StockEntry(StockController): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', filters={'parent': self.work_order}, - fields=["item_code", "required_qty", "consumed_qty"] + fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"] ) + work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: - qty = item.required_qty - item_account_details = get_item_defaults(item.item_code, self.company) # Take into account consumption if there are any. - if self.purpose == 'Manufacture': - req_qty_each = flt(item.required_qty / wo.qty) - if (flt(item.consumed_qty) != 0): - remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each) - exhaust_qty = req_qty_each * wo.produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 - else: - qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty - else: - qty = req_qty_each * flt(self.fg_completed_qty) + + wo_item_qty = item.transferred_qty or item.required_qty + + req_qty_each = ( + (flt(wo_item_qty) - flt(item.consumed_qty)) / + (flt(work_order_qty) - flt(wo.produced_qty)) + ) + + qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: self.add_to_stock_entry_detail({ @@ -1134,13 +1137,15 @@ class StockEntry(StockController): else: qty = req_qty_each * flt(self.fg_completed_qty) - elif backflushed_materials.get(item.item_code): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): qty = (qty/trans_qty) * flt(self.fg_completed_qty) + if consumed_qty: + qty -= consumed_qty + if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): qty = frappe.utils.ceil(qty) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 4c7828b873..3b9608b805 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -55,7 +55,7 @@ class StockSettings(Document): """) if sle: - frappe.throw(_("Can't change valuation method, as there are transactions against some items which does not have it's own valuation method")) + frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method")) def validate_clean_description_html(self): if int(self.clean_description_html or 0) \ diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 1339d9b682..ccd01001bb 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items): select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, - sle.item_code as name, sle.voucher_no + sle.item_code as name, sle.voucher_no, sle.stock_value from `tabStock Ledger Entry` sle force index (posting_sort_index) where sle.docstatus < 2 %s %s @@ -197,7 +197,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value_difference) + value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index fe01d4b983..086755be51 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,6 +1,13 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; + frm.set_query("customer", function () { + return { + filters: { + "disabled": 0 + } + }; + }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { @@ -21,14 +28,14 @@ frappe.ui.form.on("Issue", { }, callback: function (r) { if (r && r.message) { - frm.set_query('priority', function() { + frm.set_query("priority", function() { return { filters: { "name": ["in", r.message.priority], } }; }); - frm.set_query('service_level_agreement', function() { + frm.set_query("service_level_agreement", function() { return { filters: { "name": ["in", r.message.service_level_agreements], @@ -45,9 +52,9 @@ frappe.ui.form.on("Issue", { if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.service_level_agreement) { frappe.call({ - 'method': 'frappe.client.get', + "method": "frappe.client.get", args: { - doctype: 'Service Level Agreement', + doctype: "Service Level Agreement", name: frm.doc.service_level_agreement }, callback: function(data) { @@ -127,8 +134,8 @@ frappe.ui.form.on("Issue", { reset_sla.clear(); frappe.show_alert({ - indicator: 'green', - message: __('Resetting Service Level Agreement.') + indicator: "green", + message: __("Resetting Service Level Agreement.") }); frm.call("reset_service_level_agreement", { @@ -145,35 +152,36 @@ frappe.ui.form.on("Issue", { reset_sla.show(); }, + timeline_refresh: function(frm) { // create button for "Help Article" - if(frappe.model.can_create('Help Article')) { + if (frappe.model.can_create("Help Article")) { // Removing Help Article button if exists to avoid multiple occurance frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove(); $('') .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) - .on('click', function() { - var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html(); - var doc = frappe.model.get_new_doc('Help Article'); + .on("click", function() { + var content = $(this).parents(".timeline-item:first").find(".timeline-item-content").html(); + var doc = frappe.model.get_new_doc("Help Article"); doc.title = frm.doc.subject; doc.content = content; - frappe.set_route('Form', 'Help Article', doc.name); + frappe.set_route("Form", "Help Article", doc.name); }); } - if (!frm.timeline.wrapper.find('.btn-split-issue').length) { + if (!frm.timeline.wrapper.find(".btn-split-issue").length) { let split_issue = __("Split Issue") $(``) .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) if (!frm.timeline.wrapper.data("split-issue-event-attached")){ - frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => { + frm.timeline.wrapper.on("click", ".btn-split-issue", (e) => { var dialog = new frappe.ui.Dialog({ title: __("Split Issue"), fields: [ - {fieldname: 'subject', fieldtype: 'Data', reqd:1, label: __('Subject'), description: __('All communications including and above this shall be moved into the new Issue')} + {fieldname: "subject", fieldtype: "Data", reqd: 1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")} ], primary_action_label: __("Split"), primary_action: function() { @@ -226,7 +234,7 @@ function set_time_to_resolve_and_response(frm) { function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(moment()); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; + let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green"; return {"diff_display": diff_display, "indicator": indicator}; } diff --git a/erpnext/telephony/doctype/voice_call_settings/__init__.py b/erpnext/telephony/doctype/voice_call_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py new file mode 100644 index 0000000000..85d6adda09 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVoiceCallSettings(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js new file mode 100644 index 0000000000..4a61b612d0 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Voice Call Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json new file mode 100644 index 0000000000..25e55a22dc --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2020-12-08 16:52:40.590146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "call_receiving_device", + "column_break_3", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "permlevel": 1, + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "default": "Computer", + "fieldname": "call_receiving_device", + "fieldtype": "Select", + "label": "Call Receiving Device", + "options": "Computer\nPhone" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-14 18:49:34.600194", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Voice Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py new file mode 100644 index 0000000000..ad3bbf1784 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class VoiceCallSettings(Document): + pass diff --git a/erpnext/templates/print_formats/includes/taxes.html b/erpnext/templates/print_formats/includes/taxes.html index 6e984f3901..304e845287 100644 --- a/erpnext/templates/print_formats/includes/taxes.html +++ b/erpnext/templates/print_formats/includes/taxes.html @@ -20,10 +20,10 @@ {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
    -
    + +
    - {{ frappe.format_value(frappe.utils.flt(charge.tax_amount), - table_meta.get_field("tax_amount"), doc, currency=doc.currency) }} + {{ charge.get_formatted('tax_amount', doc) }}
    {%- endif -%}