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/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/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1d41d0fa2a..7830cfd370 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", + } + }; + }); }, onload: function() { this._super(); 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 8bd788890a..d94d261c6b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -206,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}"). @@ -233,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) @@ -449,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) @@ -457,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): @@ -474,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) @@ -526,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"], @@ -546,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( @@ -832,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) @@ -876,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 @@ -1095,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 502e65ed8d..5efc32e11d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + 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 81f425f868..ca6f22cc30 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -758,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) @@ -777,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")) @@ -816,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"): @@ -838,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 \ @@ -1265,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" @@ -1530,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) @@ -1559,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, @@ -1567,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 46e954d948..22a4f33654 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1573,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 @@ -1607,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) @@ -1781,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({ @@ -2039,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/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/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/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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 93a79ec934..32c5d3a3b1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -107,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) @@ -932,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 5fabf7017b..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: diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 2555edf06b..8c05134ae4 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -254,22 +254,26 @@ 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): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2f7b361b39..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'), diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index ad58f137ee..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']) 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/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/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/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/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index cf07698ad6..f4877fdca0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = { "allDay": "allDay", "progress": "progress" }, - gantt: true, + gantt: { + field_map: { + "start": "started_time", + "end": "started_time", + "id": "name", + "title": "subject", + "color": "color", + "allDay": "allDay", + "progress": "progress" + } + }, filters: [ { "fieldtype": "Link", 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/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 435511210b..6311274316 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -450,7 +450,6 @@ erpnext.patches.v8_9.set_member_party_type erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order erpnext.patches.v9_0.student_admission_childtable_migrate -erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23 erpnext.patches.v9_0.add_healthcare_domain erpnext.patches.v9_0.set_variant_item_description erpnext.patches.v9_0.set_uoms_in_variant_field 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/v9_0/fix_subscription_next_date.py b/erpnext/patches/v9_0/fix_subscription_next_date.py deleted file mode 100644 index 4595c8dc99..0000000000 --- a/erpnext/patches/v9_0/fix_subscription_next_date.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import getdate -from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date - -def execute(): - frappe.reload_doc('accounts', 'doctype', 'subscription') - fields = ["name", "reference_doctype", "reference_document", - "start_date", "frequency", "repeat_on_day"] - - for d in fields: - if not frappe.db.has_column('Subscription', d): - return - - doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice') - for data in frappe.get_all('Subscription', - fields = fields, - filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}): - - recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id") - if recurring_id: - frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s" - .format(data.reference_doctype), (data.name, recurring_id)) - - date_field = 'transaction_date' - if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']: - date_field = 'posting_date' - - start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field) - - if start_date and getdate(start_date) != getdate(data.start_date): - last_ref_date = frappe.db.sql(""" - select {0} - from `tab{1}` - where subscription=%s and docstatus < 2 - order by creation desc - limit 1 - """.format(date_field, data.reference_doctype), data.name)[0][0] - - next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day) - - frappe.db.set_value("Subscription", data.name, { - "start_date": start_date, - "next_schedule_date": next_schedule_date - }, None) \ No newline at end of file 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/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_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/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 7f08cd1359..3bc20f8733 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -408,7 +408,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 @@ -492,7 +492,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); }, @@ -509,11 +509,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/india/taxes.js b/erpnext/regional/india/taxes.js index b70b2ec48c..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; 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.py b/erpnext/selling/doctype/sales_order/sales_order.py index 04d85e575c..9388e0927e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -14,7 +14,6 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty from frappe.desk.notifications import clear_doctype_notifications from frappe.contacts.doctype.address.address import get_company_address from erpnext.controllers.selling_controller import SellingController -from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -418,8 +417,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/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/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca..27fcbb7e2a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -841,6 +841,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) @@ -850,6 +854,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/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 -%}