diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a181c2d42c..c90e01cfbd 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.7.1' +__version__ = '13.8.0' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 1be2fbf5c8..f763df0852 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -230,7 +230,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction can not be converted to group.")) elif self.account_type and not self.flags.exclude_account_type_check: - throw(_("Cannot covert to Group because Account Type is selected.")) + throw(_("Cannot convert to Group because Account Type is selected.")) else: self.is_group = 1 self.save() diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 4fd8413d83..8456b49c8e 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -391,5 +391,5 @@ def set_default_accounts(company): }) company.save() - install_country_fixtures(company.name) + install_country_fixtures(company.name, company.country) company.create_default_tax_template() diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 6635128f9e..d788d91855 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ] }) - + jv.flags.ignore_mandatory = True jv.submit() \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 7459c11d4d..33c3e0432b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1545,6 +1545,7 @@ "fieldname": "consolidated_invoice", "fieldtype": "Link", "label": "Consolidated Sales Invoice", + "no_copy": 1, "options": "Sales Invoice", "read_only": 1 } @@ -1552,7 +1553,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-02-01 15:03:33.800707", + "modified": "2021-07-29 13:37:20.636171", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ffe8be1162..3173db13af 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -15,6 +15,7 @@ from erpnext.healthcare.doctype.lab_test_template.lab_test_template import make_ class TestPricingRule(unittest.TestCase): def setUp(self): delete_existing_pricing_rules() + setup_pricing_rule_data() def tearDown(self): delete_existing_pricing_rules() @@ -554,6 +555,8 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() +test_dependencies = ["Campaign"] + def make_pricing_rule(**args): args = frappe._dict(args) @@ -600,6 +603,13 @@ def make_pricing_rule(**args): if args.get(applicable_for): doc.db_set(applicable_for, args.get(applicable_for)) +def setup_pricing_rule_data(): + if not frappe.db.exists('Campaign', '_Test Campaign'): + frappe.get_doc({ + 'doctype': 'Campaign', + 'campaign_name': '_Test Campaign', + 'name': '_Test Campaign' + }).insert() def delete_existing_pricing_rules(): for doctype in ["Pricing Rule", "Pricing Rule Item Code", diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b54d0e73a8..94abf3b3c0 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) if parenttype in ["Customer Group", "Item Group", "Territory"]: parent_field = "parent_{0}".format(frappe.scrub(parenttype)) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 54b10f583f..87ab31f0d5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. }, get_query_filters: { docstatus: 1, - status: ["not in", ["Closed", "Completed"]], + status: ["not in", ["Closed", "Completed", "Return Issued"]], company: me.frm.doc.company, is_return: 0 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b99d75ec49..863c104dff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -27,6 +27,8 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost +class WarehouseMissingError(frappe.ValidationError): pass + form_grid_templates = { "items": "templates/form_grid/item_grid.html" } @@ -207,8 +209,8 @@ class PurchaseInvoice(BuyingController): 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}"). - format(d.idx, d.item_code, self.company)) + frappe.throw(_("Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"). + format(d.idx, d.item_code, self.company), exc=WarehouseMissingError) super(PurchaseInvoice, self).validate_warehouse() @@ -246,7 +248,7 @@ class PurchaseInvoice(BuyingController): and (not item.po_detail or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")): - if self.update_stock and (not item.from_warehouse): + if self.update_stock and item.warehouse and (not item.from_warehouse): if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]: msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format( item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse)) @@ -657,7 +659,7 @@ class PurchaseInvoice(BuyingController): ) gl_entries.append( self.get_gl_dict({ - "account": self.get_company_default("exchange_gain_loss_account"), + "account": self.get_company_default("exchange_gain_loss_account"), "against": self.supplier, "credit": discrepancy_caused_by_exchange_rate_difference, "cost_center": item.cost_center, @@ -1193,7 +1195,7 @@ def get_purchase_document_details(doc): purchase_receipts_or_invoices.append(item.get(doc_reference)) if item.get(items_reference): items.append(item.get(items_reference)) - + exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index db6f143eb8..e90b35fc6a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -240,7 +240,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.conversion_rate = 80 pi.insert() - pi.submit() + pi.submit() # Get exchnage gain and loss account exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account') @@ -978,7 +978,7 @@ class TestPurchaseInvoice(unittest.TestCase): unlink_enabled = frappe.db.get_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice") - + frappe.db.set_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) @@ -1018,8 +1018,8 @@ class TestPurchaseInvoice(unittest.TestCase): expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], - ["_Test Payable USD - _TC", -40000.0], - ["Exchange Gain/Loss - _TC", 2500.0] + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -2500.0] ] gl_entries = frappe.db.sql(""" @@ -1027,7 +1027,7 @@ class TestPurchaseInvoice(unittest.TestCase): where voucher_no=%s group by account order by account asc""", (pi.name), as_dict=1) - + for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) @@ -1049,8 +1049,8 @@ class TestPurchaseInvoice(unittest.TestCase): expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], - ["_Test Payable USD - _TC", -38000.0], - ["Exchange Gain/Loss - _TC", 1500.0] + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -1500.0] ] gl_entries = frappe.db.sql(""" diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c6e6e3da6f..be20b18bea 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2,13 +2,14 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext import unittest, copy, time from frappe.utils import nowdate, flt, getdate, cint, add_days, add_months from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice +from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency @@ -1073,7 +1074,7 @@ class TestSalesInvoice(unittest.TestCase): def test_gle_made_when_asset_is_returned(self): create_asset_data() asset = create_asset(item_code="Macbook Pro") - + si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) @@ -1081,7 +1082,7 @@ class TestSalesInvoice(unittest.TestCase): # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 loss_for_si = frappe.get_all( - "GL Entry", + "GL Entry", filters = { "voucher_no": si.name, "account": disposal_account @@ -1090,7 +1091,7 @@ class TestSalesInvoice(unittest.TestCase): )[0] loss_for_return_si = frappe.get_all( - "GL Entry", + "GL Entry", filters = { "voucher_no": return_si.name, "account": disposal_account @@ -1836,6 +1837,89 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_inter_company_transaction_without_default_warehouse(self): + "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." + # setup + old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) + + old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled('_Test Company 1') + frappe.local.enable_perpetual_inventory['_Test Company 1'] = 1 + + frappe.db.set_value("Company", '_Test Company 1', "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1") + frappe.db.set_value("Company", '_Test Company 1', "expenses_included_in_valuation", "Expenses Included In Valuation - _TC1") + + + if not frappe.db.exists("Customer", "_Test Internal Customer"): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": "_Test Internal Customer", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": "_Test Company 1" + }) + + customer.append("companies", { + "company": "Wind Power LLC" + }) + + customer.insert() + + if not frappe.db.exists("Supplier", "_Test Internal Supplier"): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Internal Supplier", + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": "Wind Power LLC" + }) + + supplier.append("companies", { + "company": "_Test Company 1" + }) + + supplier.insert() + + # begin test + si = create_sales_invoice( + company = "Wind Power LLC", + customer = "_Test Internal Customer", + debit_to = "Debtors - WP", + warehouse = "Stores - WP", + income_account = "Sales - WP", + expense_account = "Cost of Goods Sold - WP", + cost_center = "Main - WP", + currency = "USD", + update_stock = 1, + do_not_save = 1 + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + + # in absence of warehouse Stock Received But Not Billed is set as expense account while mapping + # mapping is not obstructed + self.assertIsNone(target_doc.items[0].warehouse) + self.assertEqual(target_doc.items[0].expense_account, "Stock Received But Not Billed - _TC1") + + target_doc.items[0].update({"cost_center": "Main - _TC1"}) + + # missing warehouse is validated on save, after mapping + self.assertRaises(WarehouseMissingError, target_doc.save) + + target_doc.items[0].update({"warehouse": "Stores - _TC1"}) + target_doc.save() + + # after warehouse is set, linked account or default inventory account is set + self.assertEqual(target_doc.items[0].expense_account, 'Stock In Hand - _TC1') + + # tear down + frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) + def test_internal_transfer_gl_entry(self): ## Create internal transfer account account = create_account(account_name="Unrealized Profit", diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/__init__.py b/erpnext/accounts/doctype/south_africa_vat_account/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/shopify_log/__init__.py rename to erpnext/accounts/doctype/south_africa_vat_account/__init__.py diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json new file mode 100644 index 0000000000..fa1aa7da59 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "autoname": "account", + "creation": "2021-07-08 22:04:24.634967", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "label": "Account", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-08 22:35:33.202911", + "modified_by": "Administrator", + "module": "Accounts", + "name": "South Africa VAT Account", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py new file mode 100644 index 0000000000..4bd8c65a04 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class SouthAfricaVATAccount(Document): + pass diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json index f9160e281d..153906ffe9 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json @@ -1,263 +1,151 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 18:42:06.431683", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 18:42:06.431683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "category_details_section", + "category_name", + "round_off_tax_amount", + "column_break_2", + "consider_party_ledger_amount", + "tax_on_excess_amount", + "section_break_8", + "rates", + "section_break_7", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "category_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Category Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_8", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Tax Withholding Rates", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rates", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Rates", - "length": 0, - "no_copy": 0, "options": "Tax Withholding Rate", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldname": "section_break_7", + "fieldtype": "Section Break", "label": "Account Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Tax Withholding Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Tax Withholding Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "category_details_section", + "fieldtype": "Section Break", + "label": "Category Details", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach", + "fieldname": "consider_party_ledger_amount", + "fieldtype": "Check", + "label": "Consider Entire Party Ledger Amount", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Tax will be withheld only for amount exceeding the cumulative threshold", + "fieldname": "tax_on_excess_amount", + "fieldtype": "Check", + "label": "Only Deduct Tax On Excess Amount ", + "show_days": 1, + "show_seconds": 1 + }, + { + "description": "Checking this will round off the tax amount to the nearest integer", + "fieldname": "round_off_tax_amount", + "fieldtype": "Check", + "label": "Round Off Tax Amount", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-17 22:53:26.193179", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Withholding Category", - "name_case": "", - "owner": "Administrator", + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-27 21:47:34.396071", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withholding Category", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b9ee4a0963..481ef285e7 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, getdate +from frappe.utils import flt, getdate, cint from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): @@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category + "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, + "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, + "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, + "round_off_tax_amount": tax_withholding.round_off_tax_amount }) def get_tax_withholding_rates(tax_withholding, fiscal_year): @@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): fiscal_year = fiscal_year_details[0] + vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers @@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 + invoice_filters = { + 'name': ('in', vouchers), + 'docstatus': 1 + } - supp_credit_amt = frappe.db.get_value('Purchase Invoice', { - 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1 - }, 'sum(net_total)') or 0.0 + field = 'sum(net_total)' + + if not cint(tax_details.consider_party_ledger_amount): + invoice_filters.update({'apply_tds': 1}) + field = 'sum(grand_total)' + + supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { 'parent': ('in', vouchers), 'docstatus': 1, @@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu cumulative_threshold = tax_details.get('cumulative_threshold', 0) if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): + # Get net total again as TDS is calculated on net total + # Grand is used to just check for threshold breach + net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + net_total += inv.net_total + supp_credit_amt = net_total - cumulative_threshold + if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, @@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) else: tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 + + if cint(tax_details.round_off_tax_amount): + tds_amount = round(tds_amount) return tds_amount diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index dd26be7c99..2ba22ca435 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tax_withholding_category_checks(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + + # First Invoice with no tds check + pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True) + pi.apply_tds = 0 + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000) + pi1.submit() + invoices.append(pi1) + + # Cumulative threshold is 30000 + # Threshold calculation should be on both the invoices + # TDS should be applied only on 1000 + self.assertEqual(pi1.taxes[0].tax_amount, 1000) + + for d in invoices: + d.cancel() + + def test_cumulative_threshold_tcs(self): frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") invoices = [] @@ -195,7 +220,7 @@ def create_sales_invoice(**args): def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: + for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']: if frappe.db.exists('Supplier', name): continue @@ -311,3 +336,23 @@ def create_tax_with_holding_category(): 'account': 'TDS - _TC' }] }).insert() + + if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): + frappe.get_doc({ + "doctype": "Tax Withholding Category", + "name": "New TDS Category", + "category_name": "New TDS Category", + "round_off_tax_amount": 1, + "consider_party_ledger_amount": 1, + "tax_on_excess_amount": 1, + "rates": [{ + 'fiscal_year': fiscal_year, + 'tax_withholding_rate': 10, + 'single_threshold': 0, + 'cumulative_threshold': 30000 + }], + "accounts": [{ + 'company': '_Test Company', + 'account': 'TDS - _TC' + }] + }).insert() diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 84c74543da..6d8623c189 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -241,6 +241,7 @@ class GrossProfitGenerator(object): sle.voucher_detail_no == row.item_row: previous_stock_value = len(my_sle) > i+1 and \ flt(my_sle[i+1].stock_value) or 0.0 + if previous_stock_value: return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: @@ -335,7 +336,7 @@ class GrossProfitGenerator(object): res = frappe.db.sql("""select item_code, voucher_type, voucher_no, voucher_detail_no, stock_value, warehouse, actual_qty as qty from `tabStock Ledger Entry` - where company=%(company)s + where company=%(company)s and is_cancelled = 0 order by item_code desc, warehouse desc, posting_date desc, posting_time desc, creation desc""", self.filters, as_dict=True) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 1cdbd8d38a..9272bc4fce 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -566,10 +566,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) @frappe.whitelist() -def get_company_default(company, fieldname): - value = frappe.get_cached_value('Company', company, fieldname) +def get_company_default(company, fieldname, ignore_validation=False): + value = frappe.get_cached_value('Company', company, fieldname) - if not value: + if not ignore_validation and not value: throw(_("Please set default {0} in Company {1}") .format(frappe.get_meta("Company").get_label(fieldname), company)) @@ -966,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): for e in existing_gle: if entry.account == e.account: account_existed = True - if (entry.account == e.account and entry.against_account == e.against_account + if (entry.account == e.account and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and ( flt(entry.debit, precision) != flt(e.debit, precision) or flt(entry.credit, precision) != flt(e.credit, precision))): diff --git a/erpnext/change_log/v13/v13_8_0.md b/erpnext/change_log/v13/v13_8_0.md new file mode 100644 index 0000000000..98ed95ae04 --- /dev/null +++ b/erpnext/change_log/v13/v13_8_0.md @@ -0,0 +1,39 @@ +# Version 13.8.0 Release Notes + +### Features & Enhancements +- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222)) +- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677)) +- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564)) + +### Fixes +- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600)) +- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651)) +- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460)) +- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438)) +- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206)) +- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487)) +- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502)) +- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644)) +- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431)) +- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508)) +- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420)) +- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558)) +- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712)) +- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721)) +- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691)) +- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518)) +- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646)) +- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727)) +- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294)) +- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292)) +- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436)) +- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412)) +- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613)) +- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614)) +- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734)) +- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485)) +- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560)) +- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576)) +- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643)) +- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418)) +- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498)) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 913e70b30c..c793c19a92 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -674,19 +674,24 @@ class AccountsController(TransactionBase): if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: for d in self.get("advances"): if d.exchange_gain_loss: - party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer - party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to - party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" + is_purchase_invoice = self.get('doctype') == 'Purchase Invoice' + party = self.supplier if is_purchase_invoice else self.customer + party_account = self.credit_to if is_purchase_invoice else self.debit_to + party_type = "Supplier" if is_purchase_invoice else "Customer" gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + if not gain_loss_account: + frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}") + .format(self.get('company'))) account_currency = get_account_currency(gain_loss_account) if account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)) # for purchase dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' - # just reverse for sales? - dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + if not is_purchase_invoice: + # just reverse for sales? + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' gl_entries.append( self.get_gl_dict({ @@ -904,9 +909,9 @@ class AccountsController(TransactionBase): frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") .format(item.item_code, item.idx, max_allowed_amt)) - def get_company_default(self, fieldname): + def get_company_default(self, fieldname, ignore_validation=False): from erpnext.accounts.utils import get_company_default - return get_company_default(self.company, fieldname) + return get_company_default(self.company, fieldname, ignore_validation=ignore_validation) def get_stock_items(self): stock_items = [] diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 280319321f..21c052a391 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): INNER JOIN `tabBatch` batch on sle.batch_no = batch.name where batch.disabled = 0 + and sle.is_cancelled = 0 and sle.item_code = %(item_code)s and sle.warehouse = %(warehouse)s and (sle.batch_no like %(txt)s diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 43e1b99f3a..e9a7a95fc7 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, + status:function(frm){ + if (frm.doc.status == "Lost"){ + frm.trigger('set_as_lost_dialog'); + } + + }, + customer_address: function(frm, cdt, cdn) { erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false); }, @@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", { frm.add_custom_button(__('Quotation'), cur_frm.cscript.create_quotation, __('Create')); - if(doc.status!=="Quotation") { - frm.add_custom_button(__('Lost'), () => { - frm.trigger('set_as_lost_dialog'); - }); - } } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { diff --git a/erpnext/education/api.py b/erpnext/education/api.py index afa0be9b9f..4493a3fef1 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -34,11 +34,14 @@ def enroll_student(source_name): } }}, ignore_permissions=True) student.save() + + student_applicant = frappe.db.get_value("Student Applicant", source_name, + ["student_category", "program"], as_dict=True) program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment.student = student.name - program_enrollment.student_category = student.student_category + program_enrollment.student_category = student_applicant.student_category program_enrollment.student_name = student.title - program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") + program_enrollment.program = student_applicant.program frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) return program_enrollment diff --git a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json index 9be292b65e..1d7497387f 100644 --- a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json +++ b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json @@ -1,195 +1,68 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-06-10 03:29:02.539914", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-06-10 03:29:02.539914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "student_applicant", + "student", + "student_name", + "column_break_3", + "student_batch_name", + "student_category" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student_applicant", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Student Applicant", - "length": 0, - "no_copy": 0, - "options": "Student Applicant", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "student_applicant", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Applicant", + "options": "Student Applicant" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Student", - "length": 0, - "no_copy": 0, - "options": "Student", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "student", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student", + "options": "Student" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Student Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "student_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Student Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_batch_name", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Student Batch Name", - "length": 0, - "no_copy": 0, - "options": "Student Batch Name", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "student_batch_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Batch Name", + "options": "Student Batch Name" + }, + { + "fieldname": "student_category", + "fieldtype": "Link", + "label": "Student Category", + "options": "Student Category", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-01-02 12:03:53.890741", - "modified_by": "Administrator", - "module": "Education", - "name": "Program Enrollment Tool Student", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-07-29 18:19:54.471594", + "modified_by": "Administrator", + "module": "Education", + "name": "Program Enrollment Tool Student", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py deleted file mode 100644 index 5d5b2e19ce..0000000000 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ /dev/null @@ -1,353 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ -import json -from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime -from erpnext.erpnext_integrations.utils import validate_webhooks_request -from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice -from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify -from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer -from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data -from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header - -@frappe.whitelist(allow_guest=True) -@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret') -def store_request_data(order=None, event=None): - if frappe.request: - order = json.loads(frappe.request.data) - event = frappe.request.headers.get('X-Shopify-Topic') - - dump_request_data(order, event) - -def sync_sales_order(order, request_id=None, old_order_sync=False): - frappe.set_user('Administrator') - shopify_settings = frappe.get_doc("Shopify Settings") - frappe.flags.request_id = request_id - - if not frappe.db.get_value("Sales Order", filters={"shopify_order_id": cstr(order['id'])}): - try: - validate_customer(order, shopify_settings) - validate_item(order, shopify_settings) - create_order(order, shopify_settings, old_order_sync=old_order_sync) - except Exception as e: - make_shopify_log(status="Error", exception=e) - - else: - make_shopify_log(status="Success") - -def prepare_sales_invoice(order, request_id=None): - frappe.set_user('Administrator') - shopify_settings = frappe.get_doc("Shopify Settings") - frappe.flags.request_id = request_id - - try: - sales_order = get_sales_order(cstr(order['id'])) - if sales_order: - create_sales_invoice(order, shopify_settings, sales_order) - make_shopify_log(status="Success") - except Exception as e: - make_shopify_log(status="Error", exception=e, rollback=True) - -def prepare_delivery_note(order, request_id=None): - frappe.set_user('Administrator') - shopify_settings = frappe.get_doc("Shopify Settings") - frappe.flags.request_id = request_id - - try: - sales_order = get_sales_order(cstr(order['id'])) - if sales_order: - create_delivery_note(order, shopify_settings, sales_order) - make_shopify_log(status="Success") - except Exception as e: - make_shopify_log(status="Error", exception=e, rollback=True) - -def get_sales_order(shopify_order_id): - sales_order = frappe.db.get_value("Sales Order", filters={"shopify_order_id": shopify_order_id}) - if sales_order: - so = frappe.get_doc("Sales Order", sales_order) - return so - -def validate_customer(order, shopify_settings): - customer_id = order.get("customer", {}).get("id") - if customer_id: - if not frappe.db.get_value("Customer", {"shopify_customer_id": customer_id}, "name"): - create_customer(order.get("customer"), shopify_settings) - -def validate_item(order, shopify_settings): - for item in order.get("line_items"): - if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"): - sync_item_from_shopify(shopify_settings, item) - -def create_order(order, shopify_settings, old_order_sync=False, company=None): - so = create_sales_order(order, shopify_settings, company) - if so: - if order.get("financial_status") == "paid": - create_sales_invoice(order, shopify_settings, so, old_order_sync=old_order_sync) - - if order.get("fulfillments") and not old_order_sync: - create_delivery_note(order, shopify_settings, so) - -def create_sales_order(shopify_order, shopify_settings, company=None): - product_not_exists = [] - customer = frappe.db.get_value("Customer", {"shopify_customer_id": shopify_order.get("customer", {}).get("id")}, "name") - so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name") - - if not so: - items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at'))) - - if not items: - message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master' - message += "\n" + ", ".join(product_not_exists) - - make_shopify_log(status="Error", exception=message, rollback=True) - - return '' - - so = frappe.get_doc({ - "doctype": "Sales Order", - "naming_series": shopify_settings.sales_order_series or "SO-Shopify-", - "shopify_order_id": shopify_order.get("id"), - "shopify_order_number": shopify_order.get("name"), - "customer": customer or shopify_settings.default_customer, - "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(), - "delivery_date": getdate(shopify_order.get("created_at")) or nowdate(), - "company": shopify_settings.company, - "selling_price_list": shopify_settings.price_list, - "ignore_pricing_rule": 1, - "items": items, - "taxes": get_order_taxes(shopify_order, shopify_settings), - "apply_discount_on": "Grand Total", - "discount_amount": get_discounted_amount(shopify_order), - }) - - if company: - so.update({ - "company": company, - "status": "Draft" - }) - so.flags.ignore_mandatory = True - so.save(ignore_permissions=True) - so.submit() - - else: - so = frappe.get_doc("Sales Order", so) - - frappe.db.commit() - return so - -def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False): - if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\ - and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice): - - if old_order_sync: - posting_date = getdate(shopify_order.get('created_at')) - else: - posting_date = nowdate() - - si = make_sales_invoice(so.name, ignore_permissions=True) - si.shopify_order_id = shopify_order.get("id") - si.shopify_order_number = shopify_order.get("name") - si.set_posting_time = 1 - si.posting_date = posting_date - si.due_date = posting_date - si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" - si.flags.ignore_mandatory = True - set_cost_center(si.items, shopify_settings.cost_center) - si.insert(ignore_mandatory=True) - si.submit() - make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date) - frappe.db.commit() - -def set_cost_center(items, cost_center): - for item in items: - item.cost_center = cost_center - -def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None): - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account) - payment_entry.flags.ignore_mandatory = True - payment_entry.reference_no = doc.name - payment_entry.posting_date = posting_date or nowdate() - payment_entry.reference_date = posting_date or nowdate() - payment_entry.insert(ignore_permissions=True) - payment_entry.submit() - -def create_delivery_note(shopify_order, shopify_settings, so): - if not cint(shopify_settings.sync_delivery_note): - return - - for fulfillment in shopify_order.get("fulfillments"): - if not frappe.db.get_value("Delivery Note", {"shopify_fulfillment_id": fulfillment.get("id")}, "name")\ - and so.docstatus==1: - - dn = make_delivery_note(so.name) - dn.shopify_order_id = fulfillment.get("order_id") - dn.shopify_order_number = shopify_order.get("name") - dn.set_posting_time = 1 - dn.posting_date = getdate(fulfillment.get("created_at")) - dn.shopify_fulfillment_id = fulfillment.get("id") - dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-" - dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings) - dn.flags.ignore_mandatory = True - dn.save() - dn.submit() - frappe.db.commit() - -def get_fulfillment_items(dn_items, fulfillment_items, shopify_settings): - return [dn_item.update({"qty": item.get("quantity")}) for item in fulfillment_items for dn_item in dn_items\ - if get_item_code(item) == dn_item.item_code] - -def get_discounted_amount(order): - discounted_amount = 0.0 - for discount in order.get("discount_codes"): - discounted_amount += flt(discount.get("amount")) - return discounted_amount - -def get_order_items(order_items, shopify_settings, delivery_date): - items = [] - all_product_exists = True - product_not_exists = [] - - for shopify_item in order_items: - if not shopify_item.get('product_exists'): - all_product_exists = False - product_not_exists.append({'title':shopify_item.get('title'), - 'shopify_order_id': shopify_item.get('id')}) - continue - - if all_product_exists: - item_code = get_item_code(shopify_item) - items.append({ - "item_code": item_code, - "item_name": shopify_item.get("name"), - "rate": shopify_item.get("price"), - "delivery_date": delivery_date, - "qty": shopify_item.get("quantity"), - "stock_uom": shopify_item.get("uom") or _("Nos"), - "warehouse": shopify_settings.warehouse - }) - else: - items = [] - - return items - -def get_item_code(shopify_item): - item_code = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("variant_id")}, "item_code") - if not item_code: - item_code = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("product_id")}, "item_code") - if not item_code: - item_code = frappe.db.get_value("Item", {"item_name": shopify_item.get("title")}, "item_code") - - return item_code - -def get_order_taxes(shopify_order, shopify_settings): - taxes = [] - for tax in shopify_order.get("tax_lines"): - taxes.append({ - "charge_type": _("On Net Total"), - "account_head": get_tax_account_head(tax), - "description": "{0} - {1}%".format(tax.get("title"), tax.get("rate") * 100.0), - "rate": tax.get("rate") * 100.00, - "included_in_print_rate": 1 if shopify_order.get("taxes_included") else 0, - "cost_center": shopify_settings.cost_center - }) - - taxes = update_taxes_with_shipping_lines(taxes, shopify_order.get("shipping_lines"), shopify_settings) - - return taxes - -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"), - "account_head": get_tax_account_head(tax), - "description": tax["title"], - "tax_amount": tax["price"], - "cost_center": shopify_settings.cost_center - }) - - return taxes - -def get_tax_account_head(tax): - tax_title = tax.get("title").encode("utf-8") - - tax_account = frappe.db.get_value("Shopify Tax Account", \ - {"parent": "Shopify Settings", "shopify_tax": tax_title}, "tax_account") - - if not tax_account: - frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title"))) - - return tax_account - -@frappe.whitelist(allow_guest=True) -def sync_old_orders(): - frappe.set_user('Administrator') - shopify_settings = frappe.get_doc('Shopify Settings') - - if not shopify_settings.sync_missing_orders: - return - - url = get_url(shopify_settings) - session = get_request_session() - - try: - res = session.get(url, headers=get_header(shopify_settings)) - res.raise_for_status() - orders = res.json()["orders"] - - for order in orders: - if is_sync_complete(shopify_settings, order): - stop_sync(shopify_settings) - return - - sync_sales_order(order=order, old_order_sync=True) - last_order_id = order.get('id') - - if last_order_id: - shopify_settings.load_from_db() - shopify_settings.last_order_id = last_order_id - shopify_settings.save() - frappe.db.commit() - - except Exception as e: - raise e - -def stop_sync(shopify_settings): - shopify_settings.sync_missing_orders = 0 - shopify_settings.last_order_id = '' - shopify_settings.save() - frappe.db.commit() - -def get_url(shopify_settings): - last_order_id = shopify_settings.last_order_id - - if not last_order_id: - if shopify_settings.sync_based_on == 'Date': - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format( - get_datetime(shopify_settings.from_date)), shopify_settings) - else: - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format( - shopify_settings.from_order_id), shopify_settings) - else: - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings) - - return url - -def is_sync_complete(shopify_settings, order): - if shopify_settings.sync_based_on == 'Date': - return getdate(shopify_settings.to_date) < getdate(order.get('created_at')) - else: - return cstr(order.get('id')) == cstr(shopify_settings.to_order_id) - diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js deleted file mode 100644 index d3fe7d2b4d..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Shopify Log', { - refresh: function(frm) { - if (frm.doc.request_data && frm.doc.status=='Error'){ - frm.add_custom_button('Resync', function() { - frappe.call({ - method:"erpnext.erpnext_integrations.doctype.shopify_log.shopify_log.resync", - args:{ - method:frm.doc.method, - name: frm.doc.name, - request_data: frm.doc.request_data - }, - callback: function(r){ - frappe.msgprint(__("Order rescheduled for sync")) - } - }) - }).addClass('btn-primary'); - } - } -}); diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json deleted file mode 100644 index ab373eedb4..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-03-14 10:02:06.227184", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Queued", - "fieldname": "status", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "method", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Method", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "message", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "traceback", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Traceback", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "request_data", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Request Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-04-20 16:23:36.862381", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Shopify Log", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py deleted file mode 100644 index a2b6af99b2..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import json -from frappe.model.document import Document -from erpnext.erpnext_integrations.utils import get_webhook_address - -class ShopifyLog(Document): - pass - - -def make_shopify_log(status="Queued", exception=None, rollback=False): - # if name not provided by log calling method then fetch existing queued state log - make_new = False - - if not frappe.flags.request_id: - make_new = True - - if rollback: - frappe.db.rollback() - - if make_new: - log = frappe.get_doc({"doctype":"Shopify Log"}).insert(ignore_permissions=True) - else: - log = log = frappe.get_doc("Shopify Log", frappe.flags.request_id) - - log.message = get_message(exception) - log.traceback = frappe.get_traceback() - log.status = status - log.save(ignore_permissions=True) - frappe.db.commit() - -def get_message(exception): - message = None - - if hasattr(exception, 'message'): - message = exception.message - elif hasattr(exception, '__str__'): - message = exception.__str__() - else: - message = "Something went wrong while syncing" - return message - -def dump_request_data(data, event="create/order"): - event_mapper = { - "orders/create": get_webhook_address(connector_name='shopify_connection', method="sync_sales_order", exclude_uri=True), - "orders/paid" : get_webhook_address(connector_name='shopify_connection', method="prepare_sales_invoice", exclude_uri=True), - "orders/fulfilled": get_webhook_address(connector_name='shopify_connection', method="prepare_delivery_note", exclude_uri=True) - } - - log = frappe.get_doc({ - "doctype": "Shopify Log", - "request_data": json.dumps(data, indent=1), - "method": event_mapper[event] - }).insert(ignore_permissions=True) - - frappe.db.commit() - frappe.enqueue(method=event_mapper[event], queue='short', timeout=300, is_async=True, - **{"order": data, "request_id": log.name}) - -@frappe.whitelist() -def resync(method, name, request_data): - frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False) - frappe.enqueue(method=method, queue='short', timeout=300, is_async=True, - **{"order": json.loads(request_data), "request_id": name}) diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js deleted file mode 100644 index 0913ce4ef3..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log_list.js +++ /dev/null @@ -1,12 +0,0 @@ -frappe.listview_settings['Shopify Log'] = { - add_fields: ["status"], - get_indicator: function(doc) { - if(doc.status==="Success"){ - return [__("Success"), "green", "status,=,Success"]; - } else if(doc.status ==="Error"){ - return [__("Error"), "red", "status,=,Error"]; - } else if(doc.status ==="Queued"){ - return [__("Queued"), "orange", "status,=,Queued"]; - } - } -} diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js deleted file mode 100644 index d22b6d5240..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Shopify Log", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shopify Log - () => frappe.tests.make('Shopify Log', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py deleted file mode 100644 index 5892e1d6c4..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -# test_records = frappe.get_test_records('Shopify Log') - -class TestShopifyLog(unittest.TestCase): - pass diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js deleted file mode 100644 index 1574795dfa..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.provide("erpnext_integrations.shopify_settings"); - -frappe.ui.form.on("Shopify Settings", "onload", function(frm){ - frappe.call({ - method:"erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings.get_series", - callback:function(r){ - $.each(r.message, function(key, value){ - set_field_options(key, value); - }); - } - }); - erpnext_integrations.shopify_settings.setup_queries(frm); -}) - -frappe.ui.form.on("Shopify Settings", "app_type", function(frm) { - frm.toggle_reqd("api_key", (frm.doc.app_type == "Private")); - frm.toggle_reqd("password", (frm.doc.app_type == "Private")); -}) - -frappe.ui.form.on("Shopify Settings", "refresh", function(frm){ - if(!frm.doc.__islocal && frm.doc.enable_shopify === 1){ - frm.toggle_reqd("price_list", true); - frm.toggle_reqd("warehouse", true); - frm.toggle_reqd("taxes", true); - frm.toggle_reqd("company", true); - frm.toggle_reqd("cost_center", true); - frm.toggle_reqd("cash_bank_account", true); - frm.toggle_reqd("sales_order_series", true); - frm.toggle_reqd("customer_group", true); - frm.toggle_reqd("shared_secret", true); - - frm.toggle_reqd("sales_invoice_series", frm.doc.sync_sales_invoice); - frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note); - - } -}) - -$.extend(erpnext_integrations.shopify_settings, { - setup_queries: function(frm) { - frm.fields_dict["warehouse"].get_query = function(doc) { - return { - filters:{ - "company": doc.company, - "is_group": "No" - } - } - } - - frm.fields_dict["taxes"].grid.get_field("tax_account").get_query = function(doc){ - return { - "query": "erpnext.controllers.queries.tax_account_query", - "filters": { - "account_type": ["Tax", "Chargeable", "Expense Account"], - "company": doc.company - } - } - } - - frm.fields_dict["cash_bank_account"].get_query = function(doc) { - return { - filters: [ - ["Account", "account_type", "in", ["Cash", "Bank"]], - ["Account", "root_type", "=", "Asset"], - ["Account", "is_group", "=",0], - ["Account", "company", "=", doc.company] - ] - } - } - - frm.fields_dict["cost_center"].get_query = function(doc) { - return { - filters:{ - "company": doc.company, - "is_group": "No" - } - } - } - - frm.fields_dict["price_list"].get_query = function() { - return { - filters:{ - "selling": 1 - } - } - } - } -}) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json deleted file mode 100644 index 308e7d163f..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json +++ /dev/null @@ -1,353 +0,0 @@ -{ - "actions": [], - "creation": "2015-05-18 05:21:07.270859", - "doctype": "DocType", - "document_type": "System", - "engine": "InnoDB", - "field_order": [ - "status_html", - "enable_shopify", - "app_type", - "column_break_4", - "last_sync_datetime", - "section_break_2", - "shopify_url", - "api_key", - "column_break_3", - "password", - "shared_secret", - "access_token", - "section_break_38", - "webhooks", - "section_break_15", - "default_customer", - "column_break_19", - "customer_group", - "company_dependent_settings", - "company", - "cash_bank_account", - "column_break_20", - "cost_center", - "erp_settings", - "price_list", - "update_price_in_erpnext_price_list", - "column_break_26", - "warehouse", - "section_break_25", - "sales_order_series", - "column_break_27", - "sync_delivery_note", - "delivery_note_series", - "sync_sales_invoice", - "sales_invoice_series", - "section_break_22", - "html_16", - "taxes", - "syncing_details_section", - "sync_missing_orders", - "sync_based_on", - "column_break_41", - "from_date", - "to_date", - "from_order_id", - "to_order_id", - "last_order_id" - ], - "fields": [ - { - "fieldname": "status_html", - "fieldtype": "HTML", - "label": "status html", - "read_only": 1 - }, - { - "default": "0", - "fieldname": "enable_shopify", - "fieldtype": "Check", - "label": "Enable Shopify" - }, - { - "default": "Private", - "fieldname": "app_type", - "fieldtype": "Data", - "in_list_view": 1, - "label": "App Type", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "last_sync_datetime", - "fieldtype": "Datetime", - "label": "Last Sync Datetime", - "read_only": 1 - }, - { - "fieldname": "section_break_2", - "fieldtype": "Section Break" - }, - { - "description": "eg: frappe.myshopify.com", - "fieldname": "shopify_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Shop URL", - "reqd": 1 - }, - { - "depends_on": "eval:doc.app_type==\"Private\"", - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key" - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.app_type==\"Private\"", - "fieldname": "password", - "fieldtype": "Password", - "label": "Password" - }, - { - "fieldname": "shared_secret", - "fieldtype": "Data", - "label": "Shared secret" - }, - { - "fieldname": "access_token", - "fieldtype": "Data", - "hidden": 1, - "label": "Access Token", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "section_break_38", - "fieldtype": "Section Break", - "label": "Webhooks Details" - }, - { - "fieldname": "webhooks", - "fieldtype": "Table", - "label": "Webhooks", - "options": "Shopify Webhook Detail", - "read_only": 1 - }, - { - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "label": "Customer Settings" - }, - { - "description": "If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order", - "fieldname": "default_customer", - "fieldtype": "Link", - "label": "Default Customer", - "options": "Customer" - }, - { - "fieldname": "column_break_19", - "fieldtype": "Column Break" - }, - { - "description": "Customer Group will set to selected group while syncing customers from Shopify", - "fieldname": "customer_group", - "fieldtype": "Link", - "label": "Customer Group", - "options": "Customer Group" - }, - { - "fieldname": "company_dependent_settings", - "fieldtype": "Section Break" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "For Company", - "options": "Company" - }, - { - "description": "Cash Account will used for Sales Invoice creation", - "fieldname": "cash_bank_account", - "fieldtype": "Link", - "label": "Cash/Bank Account", - "options": "Account" - }, - { - "fieldname": "column_break_20", - "fieldtype": "Column Break" - }, - { - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center" - }, - { - "fieldname": "erp_settings", - "fieldtype": "Section Break" - }, - { - "fieldname": "price_list", - "fieldtype": "Link", - "label": "Price List", - "options": "Price List" - }, - { - "default": "0", - "fieldname": "update_price_in_erpnext_price_list", - "fieldtype": "Check", - "label": "Update Price from Shopify To ERPNext Price List" - }, - { - "fieldname": "column_break_26", - "fieldtype": "Column Break" - }, - { - "description": "Default Warehouse to to create Sales Order and Delivery Note", - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "section_break_25", - "fieldtype": "Section Break" - }, - { - "fieldname": "sales_order_series", - "fieldtype": "Select", - "label": "Sales Order Series" - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "sync_delivery_note", - "fieldtype": "Check", - "label": "Import Delivery Notes from Shopify on Shipment" - }, - { - "depends_on": "eval:doc.sync_delivery_note==1", - "fieldname": "delivery_note_series", - "fieldtype": "Select", - "label": "Delivery Note Series" - }, - { - "default": "0", - "fieldname": "sync_sales_invoice", - "fieldtype": "Check", - "label": "Import Sales Invoice from Shopify if Payment is marked" - }, - { - "depends_on": "eval:doc.sync_sales_invoice==1", - "fieldname": "sales_invoice_series", - "fieldtype": "Select", - "label": "Sales Invoice Series" - }, - { - "fieldname": "section_break_22", - "fieldtype": "Section Break" - }, - { - "fieldname": "html_16", - "fieldtype": "HTML", - "options": "Map Shopify Taxes / Shipping Charges to ERPNext Account" - }, - { - "fieldname": "taxes", - "fieldtype": "Table", - "label": "Shopify Tax Account", - "options": "Shopify Tax Account" - }, - { - "collapsible": 1, - "fieldname": "syncing_details_section", - "fieldtype": "Section Break", - "label": "Syncing Missing Orders" - }, - { - "depends_on": "eval:doc.sync_missing_orders", - "fieldname": "last_order_id", - "fieldtype": "Data", - "label": "Last Order Id", - "read_only": 1 - }, - { - "fieldname": "column_break_41", - "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "On checking this Order from the ", - "fieldname": "sync_missing_orders", - "fieldtype": "Check", - "label": "Sync Missing Old Shopify Orders" - }, - { - "depends_on": "eval:doc.sync_missing_orders", - "fieldname": "sync_based_on", - "fieldtype": "Select", - "label": "Sync Based On", - "mandatory_depends_on": "eval:doc.sync_missing_orders", - "options": "\nDate\nShopify Order Id" - }, - { - "depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders", - "fieldname": "from_date", - "fieldtype": "Date", - "label": "From Date", - "mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders" - }, - { - "depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders", - "fieldname": "to_date", - "fieldtype": "Date", - "label": "To Date", - "mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders" - }, - { - "depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders", - "fieldname": "from_order_id", - "fieldtype": "Data", - "label": "From Order Id", - "mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders" - }, - { - "depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders", - "fieldname": "to_order_id", - "fieldtype": "Data", - "label": "To Order Id", - "mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders" - } - ], - "issingle": 1, - "links": [], - "modified": "2021-03-02 17:35:41.953317", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Shopify Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py deleted file mode 100644 index 381c5e5dec..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import json -from frappe import _ -from frappe.model.document import Document -from frappe.utils import get_request_session -from requests.exceptions import HTTPError -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from erpnext.erpnext_integrations.utils import get_webhook_address -from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log - -class ShopifySettings(Document): - def validate(self): - if self.enable_shopify == 1: - setup_custom_fields() - self.validate_access_credentials() - self.register_webhooks() - else: - self.unregister_webhooks() - - def validate_access_credentials(self): - if not (self.get_password(raise_exception=False) and self.api_key and self.shopify_url): - frappe.msgprint(_("Missing value for Password, API Key or Shopify URL"), raise_exception=frappe.ValidationError) - - def register_webhooks(self): - webhooks = ["orders/create", "orders/paid", "orders/fulfilled"] - # url = get_shopify_url('admin/webhooks.json', self) - created_webhooks = [d.method for d in self.webhooks] - url = get_shopify_url('admin/api/2021-04/webhooks.json', self) - for method in webhooks: - session = get_request_session() - try: - res = session.post(url, data=json.dumps({ - "webhook": { - "topic": method, - "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data', force_https=True), - "format": "json" - } - }), headers=get_header(self)) - res.raise_for_status() - self.update_webhook_table(method, res.json()) - - except HTTPError as e: - error_message = res.json().get('errors', e) - make_shopify_log(status="Warning", exception=error_message, rollback=True) - - except Exception as e: - make_shopify_log(status="Warning", exception=e, rollback=True) - - def unregister_webhooks(self): - session = get_request_session() - deleted_webhooks = [] - - for d in self.webhooks: - url = get_shopify_url('admin/api/2021-04/webhooks/{0}.json'.format(d.webhook_id), self) - try: - res = session.delete(url, headers=get_header(self)) - res.raise_for_status() - deleted_webhooks.append(d) - - except HTTPError as e: - error_message = res.json().get('errors', e) - make_shopify_log(status="Warning", exception=error_message, rollback=True) - - except Exception as e: - frappe.log_error(message=e, title='Shopify Webhooks Issue') - - for d in deleted_webhooks: - self.remove(d) - - def update_webhook_table(self, method, res): - self.append("webhooks", { - "webhook_id": res['webhook']['id'], - "method": method - }) - -def get_shopify_url(path, settings): - if settings.app_type == "Private": - return 'https://{}:{}@{}/{}'.format(settings.api_key, settings.get_password('password'), settings.shopify_url, path) - else: - return 'https://{}/{}'.format(settings.shopify_url, path) - -def get_header(settings): - header = {'Content-Type': 'application/json'} - - return header - -@frappe.whitelist() -def get_series(): - return { - "sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-Shopify-", - "sales_invoice_series" : frappe.get_meta("Sales Invoice").get_options("naming_series") or "SI-Shopify-", - "delivery_note_series" : frappe.get_meta("Delivery Note").get_options("naming_series") or "DN-Shopify-" - } - -def setup_custom_fields(): - custom_fields = { - "Customer": [ - dict(fieldname='shopify_customer_id', label='Shopify Customer Id', - fieldtype='Data', insert_after='series', read_only=1, print_hide=1) - ], - "Supplier": [ - dict(fieldname='shopify_supplier_id', label='Shopify Supplier Id', - fieldtype='Data', insert_after='supplier_name', read_only=1, print_hide=1) - ], - "Address": [ - dict(fieldname='shopify_address_id', label='Shopify Address Id', - fieldtype='Data', insert_after='fax', read_only=1, print_hide=1) - ], - "Item": [ - dict(fieldname='shopify_variant_id', label='Shopify Variant Id', - fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1), - dict(fieldname='shopify_product_id', label='Shopify Product Id', - fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1), - dict(fieldname='shopify_description', label='Shopify Description', - fieldtype='Text Editor', insert_after='description', read_only=1, print_hide=1) - ], - "Sales Order": [ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) - ], - "Delivery Note":[ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1), - dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1) - ], - "Sales Invoice": [ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) - ] - } - - create_custom_fields(custom_fields) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py deleted file mode 100644 index 2af57f4c89..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ - -def create_customer(shopify_customer, shopify_settings): - import frappe.utils.nestedset - - cust_name = (shopify_customer.get("first_name") + " " + (shopify_customer.get("last_name") \ - and shopify_customer.get("last_name") or "")) if shopify_customer.get("first_name")\ - else shopify_customer.get("email") - - try: - customer = frappe.get_doc({ - "doctype": "Customer", - "name": shopify_customer.get("id"), - "customer_name" : cust_name, - "shopify_customer_id": shopify_customer.get("id"), - "sync_with_shopify": 1, - "customer_group": shopify_settings.customer_group, - "territory": frappe.utils.nestedset.get_root_of("Territory"), - "customer_type": _("Individual") - }) - customer.flags.ignore_mandatory = True - customer.insert(ignore_permissions=True) - - if customer: - create_customer_address(customer, shopify_customer) - - frappe.db.commit() - - except Exception as e: - raise e - -def create_customer_address(customer, shopify_customer): - addresses = shopify_customer.get("addresses", []) - - if not addresses and "default_address" in shopify_customer: - addresses.append(shopify_customer["default_address"]) - - for i, address in enumerate(addresses): - address_title, address_type = get_address_title_and_type(customer.customer_name, i) - try : - frappe.get_doc({ - "doctype": "Address", - "shopify_address_id": address.get("id"), - "address_title": address_title, - "address_type": address_type, - "address_line1": address.get("address1") or "Address 1", - "address_line2": address.get("address2"), - "city": address.get("city") or "City", - "state": address.get("province"), - "pincode": address.get("zip"), - "country": address.get("country"), - "phone": address.get("phone"), - "email_id": shopify_customer.get("email"), - "links": [{ - "link_doctype": "Customer", - "link_name": customer.name - }] - }).insert(ignore_mandatory=True) - - except Exception as e: - raise e - -def get_address_title_and_type(customer_name, index): - address_type = _("Billing") - address_title = customer_name - if frappe.db.get_value("Address", "{0}-{1}".format(customer_name.strip(), address_type)): - address_title = "{0}-{1}".format(customer_name.strip(), index) - - return address_title, address_type diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py deleted file mode 100644 index 16efb6caee..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py +++ /dev/null @@ -1,309 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ -from erpnext import get_default_company -from frappe.utils import cstr, cint, get_request_session -from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header - -shopify_variants_attr_list = ["option1", "option2", "option3"] - -def sync_item_from_shopify(shopify_settings, item): - url = get_shopify_url("admin/api/2021-04/products/{0}.json".format(item.get("product_id")), shopify_settings) - session = get_request_session() - - try: - res = session.get(url, headers=get_header(shopify_settings)) - res.raise_for_status() - - shopify_item = res.json()["product"] - make_item(shopify_settings.warehouse, shopify_item) - except Exception as e: - raise e - -def make_item(warehouse, shopify_item): - add_item_weight(shopify_item) - - if has_variants(shopify_item): - attributes = create_attribute(shopify_item) - create_item(shopify_item, warehouse, 1, attributes) - create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list) - - else: - shopify_item["variant_id"] = shopify_item['variants'][0]["id"] - create_item(shopify_item, warehouse) - -def add_item_weight(shopify_item): - shopify_item["weight"] = shopify_item['variants'][0]["weight"] - shopify_item["weight_unit"] = shopify_item['variants'][0]["weight_unit"] - -def has_variants(shopify_item): - if len(shopify_item.get("options")) >= 1 and "Default Title" not in shopify_item.get("options")[0]["values"]: - return True - return False - -def create_attribute(shopify_item): - attribute = [] - # shopify item dict - for attr in shopify_item.get('options'): - if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"): - frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": attr.get("name"), - "item_attribute_values": [ - { - "attribute_value": attr_value, - "abbr":attr_value - } - for attr_value in attr.get("values") - ] - }).insert() - attribute.append({"attribute": attr.get("name")}) - - else: - # check for attribute values - item_attr = frappe.get_doc("Item Attribute", attr.get("name")) - if not item_attr.numeric_values: - set_new_attribute_values(item_attr, attr.get("values")) - item_attr.save() - attribute.append({"attribute": attr.get("name")}) - - else: - attribute.append({ - "attribute": attr.get("name"), - "from_range": item_attr.get("from_range"), - "to_range": item_attr.get("to_range"), - "increment": item_attr.get("increment"), - "numeric_values": item_attr.get("numeric_values") - }) - - return attribute - -def set_new_attribute_values(item_attr, values): - for attr_value in values: - if not any((d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower())\ - for d in item_attr.item_attribute_values): - item_attr.append("item_attribute_values", { - "attribute_value": attr_value, - "abbr": attr_value - }) - -def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_of=None): - item_dict = { - "doctype": "Item", - "shopify_product_id": shopify_item.get("id"), - "shopify_variant_id": shopify_item.get("variant_id"), - "variant_of": variant_of, - "sync_with_shopify": 1, - "is_stock_item": 1, - "item_code": cstr(shopify_item.get("item_code")) or cstr(shopify_item.get("id")), - "item_name": shopify_item.get("title", '').strip(), - "description": shopify_item.get("body_html") or shopify_item.get("title"), - "shopify_description": shopify_item.get("body_html") or shopify_item.get("title"), - "item_group": get_item_group(shopify_item.get("product_type")), - "has_variants": has_variant, - "attributes":attributes or [], - "stock_uom": shopify_item.get("uom") or _("Nos"), - "stock_keeping_unit": shopify_item.get("sku") or get_sku(shopify_item), - "default_warehouse": warehouse, - "image": get_item_image(shopify_item), - "weight_uom": shopify_item.get("weight_unit"), - "weight_per_unit": shopify_item.get("weight"), - "default_supplier": get_supplier(shopify_item), - "item_defaults": [ - { - "company": get_default_company() - } - ] - } - - if not is_item_exists(item_dict, attributes, variant_of=variant_of): - item_details = get_item_details(shopify_item) - name = '' - - if not item_details: - new_item = frappe.get_doc(item_dict) - new_item.insert(ignore_permissions=True, ignore_mandatory=True) - name = new_item.name - - if not name: - name = item_details.name - - if not has_variant: - add_to_price_list(shopify_item, name) - - frappe.db.commit() - -def create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list): - template_item = frappe.db.get_value("Item", filters={"shopify_product_id": shopify_item.get("id")}, - fieldname=["name", "stock_uom"], as_dict=True) - - if template_item: - for variant in shopify_item.get("variants"): - shopify_item_variant = { - "id" : variant.get("id"), - "item_code": variant.get("id"), - "title": variant.get("title"), - "product_type": shopify_item.get("product_type"), - "sku": variant.get("sku"), - "uom": template_item.stock_uom or _("Nos"), - "item_price": variant.get("price"), - "variant_id": variant.get("id"), - "weight_unit": variant.get("weight_unit"), - "weight": variant.get("weight") - } - - for i, variant_attr in enumerate(shopify_variants_attr_list): - if variant.get(variant_attr): - attributes[i].update({"attribute_value": get_attribute_value(variant.get(variant_attr), attributes[i])}) - create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name) - -def get_attribute_value(variant_attr_val, attribute): - attribute_value = frappe.db.sql("""select attribute_value from `tabItem Attribute Value` - where parent = %s and (abbr = %s or attribute_value = %s)""", (attribute["attribute"], variant_attr_val, - variant_attr_val), as_list=1) - return attribute_value[0][0] if len(attribute_value)>0 else cint(variant_attr_val) - -def get_item_group(product_type=None): - import frappe.utils.nestedset - parent_item_group = frappe.utils.nestedset.get_root_of("Item Group") - - if product_type: - if not frappe.db.get_value("Item Group", product_type, "name"): - item_group = frappe.get_doc({ - "doctype": "Item Group", - "item_group_name": product_type, - "parent_item_group": parent_item_group, - "is_group": "No" - }).insert() - return item_group.name - else: - return product_type - else: - return parent_item_group - - -def get_sku(item): - if item.get("variants"): - return item.get("variants")[0].get("sku") - return "" - -def add_to_price_list(item, name): - shopify_settings = frappe.db.get_value("Shopify Settings", None, ["price_list", "update_price_in_erpnext_price_list"], as_dict=1) - if not shopify_settings.update_price_in_erpnext_price_list: - return - - item_price_name = frappe.db.get_value("Item Price", - {"item_code": name, "price_list": shopify_settings.price_list}, "name") - - if not item_price_name: - frappe.get_doc({ - "doctype": "Item Price", - "price_list": shopify_settings.price_list, - "item_code": name, - "price_list_rate": item.get("item_price") or item.get("variants")[0].get("price") - }).insert() - else: - item_rate = frappe.get_doc("Item Price", item_price_name) - item_rate.price_list_rate = item.get("item_price") or item.get("variants")[0].get("price") - item_rate.save() - -def get_item_image(shopify_item): - if shopify_item.get("image"): - return shopify_item.get("image").get("src") - return None - -def get_supplier(shopify_item): - if shopify_item.get("vendor"): - supplier = frappe.db.sql("""select name from tabSupplier - where name = %s or shopify_supplier_id = %s """, (shopify_item.get("vendor"), - shopify_item.get("vendor").lower()), as_list=1) - - if not supplier: - supplier = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": shopify_item.get("vendor"), - "shopify_supplier_id": shopify_item.get("vendor").lower(), - "supplier_group": get_supplier_group() - }).insert() - return supplier.name - else: - return shopify_item.get("vendor") - else: - return "" - -def get_supplier_group(): - supplier_group = frappe.db.get_value("Supplier Group", _("Shopify Supplier")) - if not supplier_group: - supplier_group = frappe.get_doc({ - "doctype": "Supplier Group", - "supplier_group_name": _("Shopify Supplier") - }).insert() - return supplier_group.name - return supplier_group - -def get_item_details(shopify_item): - item_details = {} - - item_details = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("id")}, - ["name", "stock_uom", "item_name"], as_dict=1) - - if item_details: - return item_details - - else: - item_details = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("id")}, - ["name", "stock_uom", "item_name"], as_dict=1) - return item_details - -def is_item_exists(shopify_item, attributes=None, variant_of=None): - if variant_of: - name = variant_of - else: - name = frappe.db.get_value("Item", {"item_name": shopify_item.get("item_name")}) - - if name: - item = frappe.get_doc("Item", name) - item.flags.ignore_mandatory=True - - if not variant_of and not item.shopify_product_id: - item.shopify_product_id = shopify_item.get("shopify_product_id") - item.shopify_variant_id = shopify_item.get("shopify_variant_id") - item.save() - return True - - if item.shopify_product_id and attributes and attributes[0].get("attribute_value"): - if not variant_of: - variant_of = frappe.db.get_value("Item", - {"shopify_product_id": item.shopify_product_id}, "variant_of") - - # create conditions for all item attributes, - # as we are putting condition basis on OR it will fetch all items matching either of conditions - # thus comparing matching conditions with len(attributes) - # which will give exact matching variant item. - - conditions = ["(iv.attribute='{0}' and iv.attribute_value = '{1}')"\ - .format(attr.get("attribute"), attr.get("attribute_value")) for attr in attributes] - - conditions = "( {0} ) and iv.parent = it.name ) = {1}".format(" or ".join(conditions), len(attributes)) - - parent = frappe.db.sql(""" select * from tabItem it where - ( select count(*) from `tabItem Variant Attribute` iv - where {conditions} and it.variant_of = %s """.format(conditions=conditions) , - variant_of, as_list=1) - - if parent: - variant = frappe.get_doc("Item", parent[0][0]) - variant.flags.ignore_mandatory = True - - variant.shopify_product_id = shopify_item.get("shopify_product_id") - variant.shopify_variant_id = shopify_item.get("shopify_variant_id") - variant.save() - return False - - if item.shopify_product_id and item.shopify_product_id != shopify_item.get("shopify_product_id"): - return False - - return True - - else: - return False diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json deleted file mode 100644 index db6c3d5aa3..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json +++ /dev/null @@ -1,527 +0,0 @@ -[ - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Print Settings", - "fieldname": "compact_item_print", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "with_letterhead", - "label": "Compact Item Print", - "modified": "2016-06-06 15:18:17.025602", - "name": "Print Settings-compact_item_print", - "no_copy": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Customer", - "fieldname": "shopify_customer_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "naming_series", - "label": "Shopify Customer Id", - "modified": "2016-01-15 17:25:28.991818", - "name": "Customer-shopify_customer_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Address", - "fieldname": "shopify_address_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "fax", - "label": "Shopify Address Id", - "modified": "2016-01-15 17:50:52.213743", - "name": "Address-shopify_address_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Order", - "fieldname": "shopify_order_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "title", - "label": "Shopify Order Id", - "modified": "2016-01-18 09:55:50.764524", - "name": "Sales Order-shopify_order_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "shopify_product_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "item_code", - "label": "Shopify Product Id", - "modified": "2016-01-19 15:44:16.132952", - "name": "Item-shopify_product_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fieldname": "shopify_order_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "naming_series", - "label": "Shopify Order Id", - "modified": "2016-01-19 16:30:12.261797", - "name": "Sales Invoice-shopify_order_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Delivery Note", - "fieldname": "shopify_order_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "title", - "label": "Shopify Order Id", - "modified": "2016-01-19 16:30:31.201198", - "name": "Delivery Note-shopify_order_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "stock_keeping_unit", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "stock_uom", - "label": "Stock Keeping Unit", - "modified": "2015-11-10 09:29:10.854943", - "name": "Item-stock_keeping_unit", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "sync_with_shopify", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "is_stock_item", - "label": "Sync With Shopify", - "modified": "2015-10-12 15:54:31.997714", - "name": "Item-sync_with_shopify", - "no_copy": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Customer", - "fieldname": "sync_with_shopify", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "is_frozen", - "label": "Sync With Shopify", - "modified": "2015-10-01 17:31:55.758826", - "name": "Customer-sync_with_shopify", - "no_copy": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "shopify_variant_id", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "item_code", - "label": "Variant Id", - "modified": "2015-11-09 18:26:50.825858", - "name": "Item-shopify_variant_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "sync_qty_with_shopify", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "item_code", - "label": "Sync Quantity With Shopify", - "modified": "2015-12-29 08:37:46.183295", - "name": "Item-sync_qty_with_shopify", - "no_copy": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Delivery Note", - "fieldname": "shopify_fulfillment_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "title", - "label": "Shopify Fulfillment Id", - "modified": "2016-01-20 23:50:35.609543", - "name": "Delivery Note-shopify_fulfillment_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Supplier", - "fieldname": "shopify_supplier_id", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "supplier_name", - "label": "Shopify Supplier Id", - "modified": "2016-02-01 15:41:25.818306", - "name": "Supplier-shopify_supplier_id", - "no_copy": 1, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - }, - { - "allow_on_submit": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fieldname": "shopify_description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "insert_after": "section_break_11", - "label": "shopify_description", - "modified": "2016-06-15 12:15:36.325581", - "name": "Item-shopify_description", - "no_copy": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "unique": 0, - "width": null - } -] \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json deleted file mode 100644 index e91ce9abf8..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_customer.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "customer": { - "id": 2324518599, - "email": "andrew@wyatt.co.in", - "accepts_marketing": false, - "created_at": "2016-01-20T17:18:35+05:30", - "updated_at": "2016-01-20T17:22:23+05:30", - "first_name": "Andrew", - "last_name": "Wyatt", - "orders_count": 0, - "state": "disabled", - "total_spent": "0.00", - "last_order_id": null, - "note": "", - "verified_email": true, - "multipass_identifier": null, - "tax_exempt": false, - "tags": "", - "last_order_name": null, - "default_address": { - "id": 2476804295, - "first_name": "Andrew", - "last_name": "Wyatt", - "company": "Wyatt Inc.", - "address1": "B-11, Betahouse", - "address2": "Street 11, Sector 52", - "city": "Manhattan", - "province": "New York", - "country": "United States", - "zip": "10027", - "phone": "145-112211", - "name": "Andrew Wyatt", - "province_code": "NY", - "country_code": "US", - "country_name": "United States", - "default": true - }, - "addresses": [ - { - "id": 2476804295, - "first_name": "Andrew", - "last_name": "Wyatt", - "company": "Wyatt Inc.", - "address1": "B-11, Betahouse", - "address2": "Street 11, Sector 52", - "city": "Manhattan", - "province": "New York", - "country": "United States", - "zip": "10027", - "phone": "145-112211", - "name": "Andrew Wyatt", - "province_code": "NY", - "country_code": "US", - "country_name": "United States", - "default": true - } - ] - } -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json deleted file mode 100644 index 296dede786..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_item.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "product": { - "id": 4059739520, - "title": "Shopify Test Item", - "body_html": "
Hold back Spin Medallion-Set of 2
\n
\n
Finish: Plated/ Powder Coated
\n
Material: Iron
\n
Color Finish: Satin Silver, Brown Oil Rubbed, Roman Bronze
\n
Qty: 1 Set
", - "vendor": "Boa casa", - "product_type": "Curtain Accessories", - "created_at": "2016-01-18T17:16:37+05:30", - "handle": "1001624-01", - "updated_at": "2016-01-20T17:26:44+05:30", - "published_at": "2016-01-18T17:16:37+05:30", - "template_suffix": null, - "published_scope": "global", - "tags": "Category_Curtain Accessories, Type_Holdback", - "variants": [{ - "id": 13917612359, - "product_id": 4059739520, - "title": "Test BALCK Item", - "price": "499.00", - "sku": "", - "position": 1, - "grams": 0, - "inventory_policy": "continue", - "compare_at_price": null, - "fulfillment_service": "manual", - "inventory_management": "shopify", - "option1": "BLACK", - "option2": null, - "option3": null, - "created_at": "2016-01-18T17:16:37+05:30", - "updated_at": "2016-01-20T17:26:44+05:30", - "requires_shipping": true, - "taxable": true, - "barcode": "", - "inventory_quantity": -1, - "old_inventory_quantity": -1, - "image_id": 8539321735, - "weight": 0, - "weight_unit": "kg" - }, { - "id": 13917612423, - "product_id": 4059739520, - "title": "Test BLUE Item", - "price": "499.00", - "sku": "", - "position": 2, - "grams": 0, - "inventory_policy": "continue", - "compare_at_price": null, - "fulfillment_service": "manual", - "inventory_management": "shopify", - "option1": "BLUE", - "option2": null, - "option3": null, - "created_at": "2016-01-18T17:16:37+05:30", - "updated_at": "2016-01-20T17:26:44+05:30", - "requires_shipping": true, - "taxable": true, - "barcode": "", - "inventory_quantity": -1, - "old_inventory_quantity": -1, - "image_id": null, - "weight": 0, - "weight_unit": "kg" - }, { - "id": 13917612487, - "product_id": 4059739520, - "title": "Test White Item", - "price": "499.00", - "sku": "", - "position": 3, - "grams": 0, - "inventory_policy": "continue", - "compare_at_price": null, - "fulfillment_service": "manual", - "inventory_management": "shopify", - "option1": "White", - "option2": null, - "option3": null, - "created_at": "2016-01-18T17:16:37+05:30", - "updated_at": "2016-01-18T17:16:37+05:30", - "requires_shipping": true, - "taxable": true, - "barcode": "", - "inventory_quantity": 0, - "old_inventory_quantity": 0, - "image_id": null, - "weight": 0, - "weight_unit": "kg" - }], - "options": [{ - "id": 4985027399, - "product_id": 4059739520, - "name": "Colour", - "position": 1, - "values": [ - "BLACK", - "BLUE", - "White" - ] - }], - "images": [{ - "id": 8539321735, - "product_id": 4059739520, - "position": 1, - "created_at": "2016-01-18T17:16:37+05:30", - "updated_at": "2016-01-18T17:16:37+05:30", - "src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597", - "variant_ids": [ - 13917612359 - ] - }], - "image": { - "id": 8539321735, - "product_id": 4059739520, - "position": 1, - "created_at": "2016-01-18T17:16:37+05:30", - "updated_at": "2016-01-18T17:16:37+05:30", - "src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597", - "variant_ids": [ - 13917612359 - ] - } - } -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json b/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json deleted file mode 100644 index 988a2f0423..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/shopify_order.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "order": { - "id": 2414345735, - "email": "andrew@wyatt.co.in", - "closed_at": null, - "created_at": "2016-01-20T17:26:39+05:30", - "updated_at": "2016-01-20T17:27:15+05:30", - "number": 5, - "note": "", - "token": "660fed25987517b733644a8c9ec7c8e0", - "gateway": "manual", - "test": false, - "total_price": "1018.00", - "subtotal_price": "998.00", - "total_weight": 0, - "total_tax": "0.00", - "taxes_included": false, - "currency": "INR", - "financial_status": "paid", - "confirmed": true, - "total_discounts": "0.00", - "total_line_items_price": "998.00", - "cart_token": null, - "buyer_accepts_marketing": false, - "name": "#1005", - "referring_site": null, - "landing_site": null, - "cancelled_at": null, - "cancel_reason": null, - "total_price_usd": "15.02", - "checkout_token": null, - "reference": null, - "user_id": 55391175, - "location_id": null, - "source_identifier": null, - "source_url": null, - "processed_at": "2016-01-20T17:26:39+05:30", - "device_id": null, - "browser_ip": null, - "landing_site_ref": null, - "order_number": 1005, - "discount_codes": [], - "note_attributes": [], - "payment_gateway_names": [ - "manual" - ], - "processing_method": "manual", - "checkout_id": null, - "source_name": "shopify_draft_order", - "fulfillment_status": "fulfilled", - "tax_lines": [], - "tags": "", - "contact_email": "andrew@wyatt.co.in", - "line_items": [ - { - "id": 4125768135, - "variant_id": 13917612359, - "title": "Shopify Test Item", - "quantity": 1, - "price": "499.00", - "grams": 0, - "sku": "", - "variant_title": "Roman BALCK 1", - "vendor": "Boa casa", - "fulfillment_service": "manual", - "product_id": 4059739527, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "Roman BALCK 1", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 0, - "total_discount": "0.00", - "fulfillment_status": "fulfilled", - "tax_lines": [] - }, - { - "id": 4125768199, - "variant_id": 13917612423, - "title": "Shopify Test Item", - "quantity": 1, - "price": "499.00", - "grams": 0, - "sku": "", - "variant_title": "Satin BLUE 1", - "vendor": "Boa casa", - "fulfillment_service": "manual", - "product_id": 4059739527, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "Satin BLUE 1", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 0, - "total_discount": "0.00", - "fulfillment_status": "fulfilled", - "tax_lines": [] - } - ], - "shipping_lines": [ - { - "id": 2108906247, - "title": "International Shipping", - "price": "20.00", - "code": "International Shipping", - "source": "shopify", - "phone": null, - "tax_lines": [] - } - ], - "billing_address": { - "first_name": "Andrew", - "address1": "B-11, Betahouse", - "phone": "145-112211", - "city": "Manhattan", - "zip": "10027", - "province": "New York", - "country": "United States", - "last_name": "Wyatt", - "address2": "Street 11, Sector 52", - "company": "Wyatt Inc.", - "latitude": 40.8138912, - "longitude": -73.96243270000001, - "name": "Andrew Wyatt", - "country_code": "US", - "province_code": "NY" - }, - "shipping_address": { - "first_name": "Andrew", - "address1": "B-11, Betahouse", - "phone": "145-112211", - "city": "Manhattan", - "zip": "10027", - "province": "New York", - "country": "United States", - "last_name": "Wyatt", - "address2": "Street 11, Sector 52", - "company": "Wyatt Inc.", - "latitude": 40.8138912, - "longitude": -73.96243270000001, - "name": "Andrew Wyatt", - "country_code": "US", - "province_code": "NY" - }, - "fulfillments": [ - { - "id": 1849629255, - "order_id": 2414345735, - "status": "success", - "created_at": "2016-01-20T17:27:15+05:30", - "service": "manual", - "updated_at": "2016-01-20T17:27:15+05:30", - "tracking_company": null, - "tracking_number": null, - "tracking_numbers": [], - "tracking_url": null, - "tracking_urls": [], - "receipt": {}, - "line_items": [ - { - "id": 4125768199, - "variant_id": 13917612423, - "title": "1001624/01", - "quantity": 1, - "price": "499.00", - "grams": 0, - "sku": "", - "variant_title": "Satin Silver", - "vendor": "Boa casa", - "fulfillment_service": "manual", - "product_id": 4059739527, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "1001624/01 - Satin Silver", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 0, - "total_discount": "0.00", - "fulfillment_status": "fulfilled", - "tax_lines": [] - } - ] - }, - { - "id": 1849628167, - "order_id": 2414345735, - "status": "success", - "created_at": "2016-01-20T17:26:58+05:30", - "service": "manual", - "updated_at": "2016-01-20T17:26:58+05:30", - "tracking_company": null, - "tracking_number": null, - "tracking_numbers": [], - "tracking_url": null, - "tracking_urls": [], - "receipt": {}, - "line_items": [ - { - "id": 4125768135, - "variant_id": 13917612359, - "title": "1001624/01", - "quantity": 1, - "price": "499.00", - "grams": 0, - "sku": "", - "variant_title": "Roman Bronze", - "vendor": "Boa casa", - "fulfillment_service": "manual", - "product_id": 4059739527, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "1001624/01 - Roman Bronze", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 0, - "total_discount": "0.00", - "fulfillment_status": "fulfilled", - "tax_lines": [] - } - ] - } - ], - "refunds": [], - "customer": { - "id": 2324518599, - "email": "andrew@wyatt.co.in", - "accepts_marketing": false, - "created_at": "2016-01-20T17:18:35+05:30", - "updated_at": "2016-01-20T17:26:39+05:30", - "first_name": "Andrew", - "last_name": "Wyatt", - "orders_count": 1, - "state": "disabled", - "total_spent": "1018.00", - "last_order_id": 2414345735, - "note": "", - "verified_email": true, - "multipass_identifier": null, - "tax_exempt": false, - "tags": "", - "last_order_name": "#1005", - "default_address": { - "id": 2476804295, - "first_name": "Andrew", - "last_name": "Wyatt", - "company": "Wyatt Inc.", - "address1": "B-11, Betahouse", - "address2": "Street 11, Sector 52", - "city": "Manhattan", - "province": "New York", - "country": "United States", - "zip": "10027", - "phone": "145-112211", - "name": "Andrew Wyatt", - "province_code": "NY", - "country_code": "US", - "country_name": "United States", - "default": true - } - } - } -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js deleted file mode 100644 index b2f82d5a27..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Shopify Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shopify Settings - () => frappe.tests.make('Shopify Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py deleted file mode 100644 index 6bec301b8e..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals -import frappe - -import unittest, os, json -from frappe.utils import cstr, cint -from erpnext.erpnext_integrations.connectors.shopify_connection import create_order -from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item -from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer -from frappe.core.doctype.data_import.data_import import import_doc - - -class ShopifySettings(unittest.TestCase): - @classmethod - def setUpClass(cls): - frappe.set_user("Administrator") - - cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')) - if not cls.allow_negative_stock: - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) - - # use the fixture data - import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) - - frappe.reload_doctype("Customer") - frappe.reload_doctype("Sales Order") - frappe.reload_doctype("Delivery Note") - frappe.reload_doctype("Sales Invoice") - - cls.setup_shopify() - - @classmethod - def tearDownClass(cls): - if not cls.allow_negative_stock: - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) - - @classmethod - def setup_shopify(cls): - shopify_settings = frappe.get_doc("Shopify Settings") - shopify_settings.taxes = [] - - shopify_settings.update({ - "app_type": "Private", - "shopify_url": "test.myshopify.com", - "api_key": "17702c7c4452b9c5d235240b6e7a39da", - "password": "17702c7c4452b9c5d235240b6e7a39da", - "shared_secret": "17702c7c4452b9c5d235240b6e7a39da", - "price_list": "_Test Price List", - "warehouse": "_Test Warehouse - _TC", - "cash_bank_account": "Cash - _TC", - "account": "Cash - _TC", - "customer_group": "_Test Customer Group", - "cost_center": "Main - _TC", - "taxes": [ - { - "shopify_tax": "International Shipping", - "tax_account":"Legal Expenses - _TC" - } - ], - "enable_shopify": 0, - "sales_order_series": "SO-", - "sync_sales_invoice": 1, - "sales_invoice_series": "SINV-", - "sync_delivery_note": 1, - "delivery_note_series": "DN-" - }).save(ignore_permissions=True) - - cls.shopify_settings = shopify_settings - - def test_order(self): - # Create Customer - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer: - shopify_customer = json.load(shopify_customer) - create_customer(shopify_customer.get("customer"), self.shopify_settings) - - # Create Item - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item: - shopify_item = json.load(shopify_item) - make_item("_Test Warehouse - _TC", shopify_item.get("product")) - - # Create Order - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: - shopify_order = json.load(shopify_order) - - create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company") - - sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))}) - - self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id) - - # Check for customer - shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id")) - sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id") - - self.assertEqual(shopify_order_customer_id, sales_order_customer_id) - - # Check sales invoice - sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id}) - self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total) - - # Check delivery note - delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note` - where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0] - - self.assertEqual(delivery_note_count, len(shopify_order.get("order").get("fulfillments"))) diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json deleted file mode 100644 index 63c674c5c4..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2015-10-05 16:55:20.455371", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shopify_tax", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Shopify Tax/Shipping Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "ERPNext Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-04-09 11:36:49.272815", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Shopify Tax Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py b/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py deleted file mode 100644 index 74c13c0f6c..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_tax_account/shopify_tax_account.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, 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 ShopifyTaxAccount(Document): - pass diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json deleted file mode 100644 index e47ecdcc50..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-04-10 17:06:22.697427", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "webhook_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Webhook ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "method", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Method", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-04-11 12:43:09.456449", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Shopify Webhook Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py b/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py deleted file mode 100644 index e127989ce3..0000000000 --- a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/shopify_webhook_detail.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, 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 ShopifyWebhookDetail(Document): - pass diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index 4a5e54edd2..24b8e48ed6 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -40,16 +40,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Shopify Settings", - "link_to": "Shopify Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -113,4 +103,4 @@ "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [] -} \ No newline at end of file +} diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json index d258d57131..d656b3c4fe 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json @@ -30,16 +30,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Shopify Settings", - "link_to": "Shopify Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -79,4 +69,4 @@ "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [] -} \ No newline at end of file +} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 59b011d1a9..8f7c7db208 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,7 +341,6 @@ scheduler_events = { "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" ], "hourly_long": [ @@ -438,7 +437,8 @@ regional_overrides = { 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', - 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount' + 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', + 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py index f7601870fa..c2ed457984 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.py +++ b/erpnext/hr/doctype/appraisal/appraisal.py @@ -9,7 +9,7 @@ from frappe.utils import flt, getdate from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document -from erpnext.hr.utils import set_employee_name +from erpnext.hr.utils import set_employee_name, validate_active_employee class Appraisal(Document): def validate(self): @@ -19,6 +19,7 @@ class Appraisal(Document): if not self.goals: frappe.throw(_("Goals cannot be empty")) + validate_active_employee(self.employee) set_employee_name(self) self.validate_dates() self.validate_existing_appraisal() diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 3412675d81..f79f0fe418 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, get_datetime, formatdate +from erpnext.hr.utils import validate_active_employee class Attendance(Document): def validate(self): from erpnext.controllers.status_updater import validate_status validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) + validate_active_employee(self.employee) self.validate_attendance_date() self.validate_duplicate_record() self.validate_employee_status() diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py index 090d53262c..7f88fed73a 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request.py @@ -8,10 +8,11 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import date_diff, add_days, getdate from erpnext.hr.doctype.employee.employee import is_holiday -from erpnext.hr.utils import validate_dates +from erpnext.hr.utils import validate_dates, validate_active_employee class AttendanceRequest(Document): def validate(self): + validate_active_employee(self.employee) validate_dates(self, self.from_date, self.to_date) if self.half_day: if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index a6fe429be1..0d7fded921 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -7,12 +7,13 @@ import frappe from frappe import _ from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.model.document import Document -from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ +from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \ get_holidays_for_employee, create_additional_leave_ledger_entry class CompensatoryLeaveRequest(Document): def validate(self): + validate_active_employee(self.employee) validate_dates(self, self.work_from_date, self.work_end_date) if self.half_day: if not self.half_day_date: diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index fa017d9d4c..5ca47560b1 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -13,8 +13,10 @@ from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -class EmployeeUserDisabledError(frappe.ValidationError): pass -class EmployeeLeftValidationError(frappe.ValidationError): pass +class EmployeeUserDisabledError(frappe.ValidationError): + pass +class InactiveEmployeeStatusError(frappe.ValidationError): + pass class Employee(NestedSet): nsm_parent_field = 'reports_to' @@ -196,7 +198,7 @@ class Employee(NestedSet): message += "


" message += _("Please make sure the employees above report to another Active employee.") - throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) + throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee")) if not self.relieving_date: throw(_("Please enter relieving date.")) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 7d652a7366..8fc7cf1934 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -7,7 +7,7 @@ import frappe import erpnext import unittest import frappe.utils -from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError +from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError test_records = frappe.get_test_records('Employee') @@ -45,10 +45,33 @@ class TestEmployee(unittest.TestCase): employee2_doc.save() employee1_doc.reload() employee1_doc.status = 'Left' - self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) + self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) + + def test_employee_status_inactive(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + + employee = make_employee("test_employee_status@company.com") + employee_doc = frappe.get_doc("Employee", employee) + employee_doc.status = "Inactive" + employee_doc.save() + employee_doc.reload() + + make_holiday_list() + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + + frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") + salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", + employee=employee_doc.name, company=employee_doc.company) + salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name) + + self.assertRaises(InactiveEmployeeStatusError, salary_slip.save) + + def tearDown(self): + frappe.db.rollback() def make_employee(user, company=None, **kwargs): - "" if not frappe.db.get_value("User", user): frappe.get_doc({ "doctype": "User", @@ -80,4 +103,5 @@ def make_employee(user, company=None, **kwargs): employee.insert() return employee.name else: + frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active") return frappe.get_value("Employee", {"employee_name":user}, "name") diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index cb72f6b6d9..cbb3cc813b 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import flt, nowdate from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.hr.utils import validate_active_employee class EmployeeAdvanceOverPayment(frappe.ValidationError): pass @@ -18,11 +19,11 @@ class EmployeeAdvance(Document): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.set_status() def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') - self.set_status() def set_status(self): if self.docstatus == 0: @@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) if not bank_cash_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - + advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') - + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() je.voucher_type = get_voucher_type(mode_of_payment) @@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None): if mode_of_payment_type == "Bank": voucher_type = "Bank Entry" - return voucher_type \ No newline at end of file + return voucher_type diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 15fbd4e015..60ea0f9895 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -9,9 +9,11 @@ from frappe.model.document import Document from frappe import _ from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift +from erpnext.hr.utils import validate_active_employee class EmployeeCheckin(Document): def validate(self): + validate_active_employee(self.employee) self.validate_duplicate_log() self.fetch_shift() @@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): """Given a set of logs in chronological order calculates the total working hours based on the parameters. Zero is returned for all invalid cases. - + :param logs: The List of 'Employee Checkin'. :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index 83fb235f92..a3a61834c8 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -7,12 +7,11 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate -from erpnext.hr.utils import update_employee +from erpnext.hr.utils import update_employee, validate_active_employee class EmployeePromotion(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") != "Active": - frappe.throw(_("Cannot promote Employee with status Left or Inactive")) + validate_active_employee(self.employee) def before_submit(self): if getdate(self.promotion_date) > getdate(): diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py index 45d68729ce..0493306166 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral.py @@ -7,9 +7,11 @@ import frappe from frappe import _ from frappe.utils import get_link_to_form from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class EmployeeReferral(Document): def validate(self): + validate_active_employee(self.referrer) self.set_full_name() self.set_referral_bonus_payment_status() diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index 6eec9fa12a..c2007747fb 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -10,10 +10,6 @@ from frappe.utils import getdate from erpnext.hr.utils import update_employee class EmployeeTransfer(Document): - def validate(self): - if frappe.get_value("Employee", self.employee, "status") != "Active": - frappe.throw(_("Cannot transfer Employee with status Left or Inactive")) - def before_submit(self): if getdate(self.transfer_date) > getdate(): frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 5010fc3f75..95e2806aed 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -6,7 +6,7 @@ import frappe, erpnext from frappe import _ from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.model.document import Document -from erpnext.hr.utils import set_employee_name, share_doc_with_approver +from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee from erpnext.accounts.party import get_party_account from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account @@ -23,6 +23,7 @@ class ExpenseClaim(AccountsController): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.validate_advances() self.validate_sanctioned_amount() self.calculate_total_amount() @@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController): if self.task and not self.project: self.project = frappe.db.get_value("Task", self.task, "project") - def set_status(self): - self.status = { + def set_status(self, update=False): + status = { "0": "Draft", "1": "Submitted", "2": "Cancelled" @@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController): paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) precision = self.precision("grand_total") - if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 - and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ - and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Paid" + if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 + and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved': + status = "Paid" elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 1 and self.approval_status == 'Rejected': - self.status = 'Rejected' + status = 'Rejected' + + if update: + self.db_set("status", status) + else: + self.status = status def on_update(self): share_doc_with_approver(self, self.expense_approver) @@ -74,7 +79,7 @@ class ExpenseClaim(AccountsController): if self.is_paid: update_reimbursed_amount(self) - self.set_status() + self.set_status(update=True) self.update_claimed_amount_in_employee_advance() def on_cancel(self): @@ -86,7 +91,6 @@ class ExpenseClaim(AccountsController): if self.is_paid: update_reimbursed_amount(self) - self.set_status() self.update_claimed_amount_in_employee_advance() def update_claimed_amount_in_employee_advance(self): diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cee6f374fd..93fb19f4a1 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate -from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver +from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange @@ -22,6 +22,7 @@ class LeaveApplication(Document): return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) def validate(self): + validate_active_employee(self.employee) set_employee_name(self) self.validate_dates() self.validate_balance_leaves() diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index e041b7fb8f..912bd8ad92 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate, flt -from erpnext.hr.utils import set_employee_name +from erpnext.hr.utils import set_employee_name, validate_active_employee from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves @@ -15,6 +15,7 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav class LeaveEncashment(Document): def validate(self): set_employee_name(self) + validate_active_employee(self.employee) self.get_leave_details_for_encashment() self.validate_salary_structure() diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index ab65260c09..89ae4d535d 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -9,10 +9,12 @@ from frappe.model.document import Document from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday +from erpnext.hr.utils import validate_active_employee from datetime import timedelta, datetime class ShiftAssignment(Document): def validate(self): + validate_active_employee(self.employee) self.validate_overlapping_dates() if self.end_date and self.end_date <= self.start_date: diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index 177c45edc6..6461f07552 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -7,12 +7,13 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import formatdate, getdate -from erpnext.hr.utils import share_doc_with_approver +from erpnext.hr.utils import share_doc_with_approver, validate_active_employee class OverlapError(frappe.ValidationError): pass class ShiftRequest(Document): def validate(self): + validate_active_employee(self.employee) self.validate_dates() self.validate_shift_request_overlap_dates() self.validate_approver() diff --git a/erpnext/hr/doctype/travel_request/travel_request.py b/erpnext/hr/doctype/travel_request/travel_request.py index 01d3f34706..60834d3f4a 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.py +++ b/erpnext/hr/doctype/travel_request/travel_request.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class TravelRequest(Document): - pass + def validate(self): + validate_active_employee(self.employee) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 3cc1a014d7..992b18d37a 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -3,13 +3,12 @@ import erpnext import frappe -from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError from frappe import _ from frappe.desk.form import assign_to from frappe.model.document import Document from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, - get_datetime, getdate, nowdate, today, unique) - + get_datetime, getdate, nowdate, today, unique, get_link_to_form) class DuplicateDeclarationError(frappe.ValidationError): pass @@ -410,3 +409,8 @@ def share_doc_with_approver(doc, user): approver = approvers.get(doc.doctype) if doc_before_save.get(approver) != doc.get(approver): frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) + +def validate_active_employee(employee): + if frappe.db.get_value("Employee", employee, "status") == "Inactive": + frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format( + get_link_to_form("Employee", employee)), InactiveEmployeeStatusError) \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index 017026ca13..514217822e 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -14,7 +14,7 @@ frappe.ui.form.on('Loan Application', { refresh: function(frm) { frm.trigger("toggle_fields"); frm.trigger("add_toolbar_buttons"); - frm.set_query("loan_type", () => { + frm.set_query('loan_type', () => { return { filters: { company: frm.doc.company diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c68198b0e2..4e93fc6799 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -748,7 +748,7 @@ def get_valuation_rate(args): if valuation_rate <= 0: last_valuation_rate = frappe.db.sql("""select valuation_rate from `tabStock Ledger Entry` - where item_code = %s and valuation_rate > 0 + where item_code = %s and valuation_rate > 0 and is_cancelled = 0 order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a029627ab1..04f05eda13 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -45,7 +45,6 @@ erpnext.patches.v11_0.make_location_from_warehouse erpnext.patches.v11_0.make_asset_finance_book_against_old_entries erpnext.patches.v11_0.check_buying_selling_in_currency_exchange erpnext.patches.v11_0.move_item_defaults_to_child_table_for_multicompany #02-07-2018 #19-06-2019 -erpnext.patches.v11_0.refactor_erpnext_shopify #2018-09-07 erpnext.patches.v11_0.rename_overproduction_percent_field erpnext.patches.v11_0.update_backflush_subcontract_rm_based_on_bom erpnext.patches.v11_0.inter_state_field_for_gst @@ -143,7 +142,6 @@ erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account erpnext.patches.v12_0.create_default_energy_point_rules erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order -erpnext.patches.v12_0.set_default_shopify_app_type erpnext.patches.v12_0.set_cwip_and_delete_asset_settings erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings @@ -244,7 +242,6 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee execute:frappe.delete_doc("Report", "Quoted Item Comparison") erpnext.patches.v13_0.update_member_email_address -erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log @@ -297,3 +294,7 @@ erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items +erpnext.patches.v13_0.update_export_type_for_gst +erpnext.patches.v13_0.update_tds_check_field #3 +erpnext.patches.v13_0.add_custom_field_for_south_africa +erpnext.patches.v13_0.shopify_deprecation_warning diff --git a/erpnext/patches/v11_0/refactor_erpnext_shopify.py b/erpnext/patches/v11_0/refactor_erpnext_shopify.py deleted file mode 100644 index 340e9fc8bf..0000000000 --- a/erpnext/patches/v11_0/refactor_erpnext_shopify.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe.installer import remove_from_installed_apps - -def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_tax_account') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_log') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_webhook_detail') - - if 'erpnext_shopify' in frappe.get_installed_apps(): - remove_from_installed_apps('erpnext_shopify') - - frappe.delete_doc("Module Def", 'erpnext_shopify') - - frappe.db.commit() - - frappe.db.sql("truncate `tabShopify Log`") - - setup_app_type() - else: - disable_shopify() - -def setup_app_type(): - try: - shopify_settings = frappe.get_doc("Shopify Settings") - shopify_settings.app_type = 'Private' - shopify_settings.update_price_in_erpnext_price_list = 0 if getattr(shopify_settings, 'push_prices_to_shopify', None) else 1 - shopify_settings.flags.ignore_mandatory = True - shopify_settings.ignore_permissions = True - shopify_settings.save() - except Exception: - frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) - frappe.log_error(frappe.get_traceback()) - -def disable_shopify(): - # due to frappe.db.set_value wrongly written and enable_shopify being default 1 - # Shopify Settings isn't properly configured and leads to error - shopify = frappe.get_doc('Shopify Settings') - - if shopify.app_type == "Public" or shopify.app_type == None or \ - (shopify.enable_shopify and not (shopify.shopify_url or shopify.api_key)): - frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) diff --git a/erpnext/patches/v12_0/set_default_shopify_app_type.py b/erpnext/patches/v12_0/set_default_shopify_app_type.py deleted file mode 100644 index d040ea7f71..0000000000 --- a/erpnext/patches/v12_0/set_default_shopify_app_type.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import unicode_literals -import frappe - -def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.db.set_value('Shopify Settings', None, 'app_type', 'Private') \ No newline at end of file diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py new file mode 100644 index 0000000000..f882fdedf3 --- /dev/null +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.regional.south_africa.setup import make_custom_fields + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'South Africa'}) + if not company: + return + + make_custom_fields() diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index d7ad1fc696..0d8109c41a 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -30,19 +30,20 @@ def execute(): return repost_stock_entries = [] + stock_entries = frappe.db.sql_list(''' SELECT se.name FROM `tabStock Entry` se WHERE - se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s and not exists( select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 ) - Order BY + ORDER BY se.posting_date, se.posting_time - '''.format(work_orders=tuple(work_orders))) + ''', (work_orders,)) if stock_entries: print('Length of stock entries', len(stock_entries)) diff --git a/erpnext/patches/v13_0/shopify_deprecation_warning.py b/erpnext/patches/v13_0/shopify_deprecation_warning.py new file mode 100644 index 0000000000..245d1a9625 --- /dev/null +++ b/erpnext/patches/v13_0/shopify_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py deleted file mode 100644 index f1d2ea2d74..0000000000 --- a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -import frappe -from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import setup_custom_fields - -def execute(): - if frappe.db.get_single_value('Shopify Settings', 'enable_shopify'): - setup_custom_fields() diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py new file mode 100644 index 0000000000..478a2a6c80 --- /dev/null +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -0,0 +1,24 @@ +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update custom fields + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, 'default', '') + + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, 'default', '') + + # Update Customer/Supplier Masters + frappe.db.sql(""" + UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export') + """) + + frappe.db.sql(""" + UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas') + """) \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py new file mode 100644 index 0000000000..3d149586a0 --- /dev/null +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + if frappe.db.has_table("Tax Withholding Category") \ + and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): + frappe.db.sql(""" + UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 + WHERE round_off_tax_amount IS NULL + """) \ No newline at end of file diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ebeddf97f9..b978cbe2b5 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document from frappe import _, bold from frappe.utils import getdate, date_diff, comma_and, formatdate +from erpnext.hr.utils import validate_active_employee class AdditionalSalary(Document): def on_submit(self): @@ -19,6 +20,7 @@ class AdditionalSalary(Document): self.update_employee_referral(cancel=True) def validate(self): + validate_active_employee(self.employee) self.validate_dates() self.validate_salary_structure() self.validate_recurring_additional_salary_overlap() @@ -110,11 +112,11 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days +@frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): additional_salary_list = frappe.db.sql(""" - select name, salary_component as component, type, amount, - overwrite_salary_structure_amount as overwrite, - deduct_full_tax_on_selected_payroll_date + select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite, + deduct_full_tax_on_selected_payroll_date, is_recurring from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 27df30a459..5ebe514ac0 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -9,10 +9,11 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt from frappe.model.document import Document from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure -from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount +from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee class EmployeeBenefitApplication(Document): def validate(self): + validate_active_employee(self.employee) self.validate_duplicate_on_payroll_period() if not self.max_benefits: self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index d9937a7bb9..c6713f3aa4 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -8,12 +8,13 @@ from frappe import _ from frappe.utils import flt from frappe.model.document import Document from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits -from erpnext.hr.utils import get_previous_claimed_amount +from erpnext.hr.utils import get_previous_claimed_amount, validate_active_employee from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure class EmployeeBenefitClaim(Document): def validate(self): + validate_active_employee(self.employee) max_benefits = get_max_benefits(self.employee, self.claim_date) if not max_benefits or max_benefits <= 0: frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index ead3db126f..6b918ba76d 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -6,9 +6,11 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class EmployeeIncentive(Document): def validate(self): + validate_active_employee(self.employee) self.validate_salary_structure() def validate_salary_structure(self): diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index fb71a2877a..e11d60a464 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -8,11 +8,12 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import flt from frappe.model.mapper import get_mapped_doc -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \ calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionDeclaration(Document): def validate(self): + validate_active_employee(self.employee) validate_tax_declaration(self.declarations) validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) self.set_total_declared_amount() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 5bc33a65f2..8131ae0fa8 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -7,11 +7,12 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import flt -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \ calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionProofSubmission(Document): def validate(self): + validate_active_employee(self.employee) validate_tax_declaration(self.tax_exemption_proofs) self.set_total_actual_amount() self.set_total_exemption_amount() diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index 049ea265cc..055bea7410 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -7,11 +7,10 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import getdate - +from erpnext.hr.utils import validate_active_employee class RetentionBonus(Document): def validate(self): - if frappe.get_value('Employee', self.employee, 'status') != 'Active': - frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees')) + validate_active_employee(self.employee) if getdate(self.bonus_payment_date) < getdate(): frappe.throw(_('Bonus Payment Date cannot be a past date')) diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 393f647cc8..97608d72f3 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -12,6 +12,7 @@ "year_to_date", "section_break_5", "additional_salary", + "is_recurring_additional_salary", "statistical_component", "depends_on_payment_days", "exempted_from_income_tax", @@ -235,11 +236,19 @@ "label": "Year To Date", "options": "currency", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary", + "fieldname": "is_recurring_additional_salary", + "fieldtype": "Check", + "label": "Is Recurring Additional Salary", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-14 13:39:15.847158", + "modified": "2021-03-14 13:39:15.847158", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index f82b0d51bb..f0ca64fdf2 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -7,18 +7,19 @@ import datetime, math from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.model.naming import make_autoname +from frappe.utils.background_jobs import enqueue from frappe import msgprint, _ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.utilities.transaction_base import TransactionBase -from frappe.utils.background_jobs import enqueue from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry from erpnext.accounts.utils import get_fiscal_year +from erpnext.hr.utils import validate_active_employee from six import iteritems class SalarySlip(TransactionBase): @@ -39,6 +40,7 @@ class SalarySlip(TransactionBase): def validate(self): self.status = self.get_status() + validate_active_employee(self.employee) self.validate_dates() self.check_existing() if not self.salary_slip_based_on_timesheet: @@ -616,7 +618,8 @@ class SalarySlip(TransactionBase): get_salary_component_data(additional_salary.component), additional_salary.amount, component_type, - additional_salary + additional_salary, + is_recurring = additional_salary.is_recurring ) def add_tax_components(self, payroll_period): @@ -637,17 +640,20 @@ class SalarySlip(TransactionBase): tax_row = get_salary_component_data(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, component_data, amount, component_type, additional_salary=None): + def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0): component_row = None for d in self.get(component_type): if d.salary_component != component_data.salary_component: continue if ( - (not d.additional_salary - and (not additional_salary or additional_salary.overwrite)) - or (additional_salary - and additional_salary.name == d.additional_salary) + ( + not d.additional_salary + and (not additional_salary or additional_salary.overwrite) + ) or ( + additional_salary + and additional_salary.name == d.additional_salary + ) ): component_row = d break @@ -675,8 +681,13 @@ class SalarySlip(TransactionBase): component_row.set(attr, component_data.get(attr)) if additional_salary: - component_row.default_amount = 0 - component_row.additional_amount = amount + component_row.is_recurring_additional_salary = is_recurring + if additional_salary.overwrite: + component_row.additional_amount = flt(flt(amount) - flt(component_row.get("default_amount", 0)), + component_row.precision("additional_amount")) + else: + component_row.default_amount = 0 + component_row.additional_amount = amount component_row.additional_salary = additional_salary.name component_row.deduct_full_tax_on_selected_payroll_date = \ additional_salary.deduct_full_tax_on_selected_payroll_date @@ -708,6 +719,7 @@ class SalarySlip(TransactionBase): # get remaining numbers of sub-period (period for which one salary is processed) remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] + # get taxable_earnings, paid_taxes for previous period previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date, tax_slab.allow_tax_exemption) @@ -867,8 +879,16 @@ class SalarySlip(TransactionBase): if earning.is_tax_applicable: if additional_amount: - taxable_earnings += (amount - additional_amount) - additional_income += additional_amount + if not earning.is_recurring_additional_salary: + taxable_earnings += (amount - additional_amount) + additional_income += additional_amount + else: + to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date') + period = (getdate(to_date).month - getdate(self.start_date).month) + 1 + if period > 0: + taxable_earnings += (amount - additional_amount) * period + additional_income += additional_amount * period + if earning.deduct_full_tax_on_selected_payroll_date: additional_income_with_full_tax += additional_amount continue diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index 374dd7ee44..3957d834d3 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -129,7 +129,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={"account_currency": currency}), + "payment_account": get_random("Account", filters={'account_currency': currency}), "currency": currency } if other_details and isinstance(other_details, dict): diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c8bd80fca0..ae38d4ca19 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with WorkstationHolidayError) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.setup.utils import get_exchange_rate +from erpnext.hr.utils import validate_active_employee class OverlapError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass class Timesheet(Document): def validate(self): + if self.employee: + validate_active_employee(self.employee) self.set_employee_name() self.set_status() self.validate_dates() diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index ea39fe1813..0ee5b097b5 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -280,9 +280,15 @@ class GSTR3BReport(Document): if self.get('invoice_items'): # Build itemised tax for export invoices, nil and exempted where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type') - == "Without Payment of Tax"): + if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \ + == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + else: + for item in items.keys(): + if item in self.is_nil_exempt + self.is_non_gst and \ + item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) + self.items_based_on_tax_rate[invoice][0].append(item) def set_outward_taxable_supplies(self): inter_state_supply_details = {} diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/__init__.py b/erpnext/regional/doctype/south_africa_vat_settings/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/shopify_settings/__init__.py rename to erpnext/regional/doctype/south_africa_vat_settings/__init__.py diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js new file mode 100644 index 0000000000..e37a61ac85 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js @@ -0,0 +1,23 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('South Africa VAT Settings', { + refresh: function(frm) { + frm.set_query("company", function() { + return { + filters: { + country: "South Africa", + } + }; + }); + frm.set_query("account", "vat_accounts", function() { + return { + filters: { + company: frm.doc.company, + account_type: "Tax", + is_group: 0 + } + }; + }); + } +}); diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json new file mode 100644 index 0000000000..8a51829c41 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "autoname": "field:company", + "creation": "2021-07-08 22:34:33.668015", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "vat_accounts" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "vat_accounts", + "fieldtype": "Table", + "label": "VAT Accounts", + "options": "South Africa VAT Account", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-14 02:17:52.476762", + "modified_by": "Administrator", + "module": "Regional", + "name": "South Africa VAT Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor", + "share": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.py b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.py new file mode 100644 index 0000000000..d74154bfe7 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class SouthAfricaVATSettings(Document): + pass diff --git a/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py b/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py new file mode 100644 index 0000000000..1c36652ad6 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestSouthAfricaVATSettings(unittest.TestCase): + pass diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 81c7a6b9a0..f4976138ac 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -316,10 +316,6 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): - if not invoice.return_against: - frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') - .format(frappe.bold('Return Against')), title=_('Missing Field')) - invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') @@ -438,7 +434,7 @@ def make_einvoice(invoice): if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - if invoice.is_return: + if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) if invoice.transporter and not invoice.is_return: @@ -969,7 +965,7 @@ class GSPConnector(): "attached_to_doctype": doctype, "attached_to_name": docname, "attached_to_field": "qrcode_image", - "is_private": 1, + "is_private": 0, "content": qr_image.getvalue()}) _file.save() frappe.db.commit() diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 92654608da..e9372f9b8f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -641,7 +641,6 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } @@ -660,7 +659,6 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fbe47d0532..88c350ac89 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -859,4 +859,15 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) - return depreciation_amount \ No newline at end of file + return depreciation_amount + +def set_item_tax_from_hsn_code(item): + if not item.taxes and item.gst_hsn_code: + hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) + + for tax in hsn_doc.taxes: + item.append('taxes', { + 'item_tax_template': tax.item_tax_template, + 'tax_category': tax.tax_category, + 'valid_from': tax.valid_from + }) \ No newline at end of file diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index b81fa810fe..4b7309440c 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -286,7 +286,8 @@ class Gstr1Report(object): # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": + and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ + and self.invoices.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): diff --git a/erpnext/erpnext_integrations/doctype/shopify_tax_account/__init__.py b/erpnext/regional/report/vat_audit_report/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/shopify_tax_account/__init__.py rename to erpnext/regional/report/vat_audit_report/__init__.py diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.js b/erpnext/regional/report/vat_audit_report/vat_audit_report.js new file mode 100644 index 0000000000..39ef9b563a --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["VAT Audit Report"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -2), + "width": "80" + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + } + ] +}; diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.json b/erpnext/regional/report/vat_audit_report/vat_audit_report.json new file mode 100644 index 0000000000..8917e8f3c7 --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-07-09 11:07:43.473518", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-07-09 11:07:43.473518", + "modified_by": "Administrator", + "module": "Regional", + "name": "VAT Audit Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "VAT Audit Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py new file mode 100644 index 0000000000..f45ba01dea --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -0,0 +1,253 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import formatdate + +def execute(filters=None): + return VATAuditReport(filters).run() + +class VATAuditReport(object): + + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.columns = [] + self.data = [] + self.doctypes = ["Purchase Invoice", "Sales Invoice"] + + def run(self): + self.get_sa_vat_accounts() + self.get_columns() + for doctype in self.doctypes: + self.select_columns = """ + name as voucher_no, + posting_date, remarks""" + columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \ + else ", customer as party, debit_to as account" + self.select_columns += columns + + self.get_invoice_data(doctype) + + if self.invoices: + self.get_invoice_items(doctype) + self.get_items_based_on_tax_rate(doctype) + self.get_data(doctype) + + return self.columns, self.data + + def get_sa_vat_accounts(self): + self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", + filters = {"parent": self.filters.company}, pluck="account") + if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: + frappe.throw(_("Please set VAT Accounts in South Africa VAT Settings")) + + def get_invoice_data(self, doctype): + conditions = self.get_conditions() + self.invoices = frappe._dict() + + invoice_data = frappe.db.sql(""" + SELECT + {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=doctype, + where_conditions=conditions), self.filters, as_dict=1) + + for d in invoice_data: + self.invoices.setdefault(d.voucher_no, d) + + def get_invoice_items(self, doctype): + self.invoice_items = frappe._dict() + + items = frappe.db.sql(""" + SELECT + item_code, parent, taxable_value, base_net_amount, is_zero_rated + FROM + `tab%s Item` + WHERE + parent in (%s) + """ % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1) + for d in items: + if d.item_code not in self.invoice_items.get(d.parent, {}): + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { + 'net_amount': 0.0}) + self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated + + def get_items_based_on_tax_rate(self, doctype): + self.items_based_on_tax_rate = frappe._dict() + self.item_tax_rate = frappe._dict() + self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \ + else "Sales Taxes and Charges" + + self.tax_details = frappe.db.sql(""" + SELECT + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + FROM + `tab%s` + WHERE + parenttype = %s and docstatus = 1 + and parent in (%s) + ORDER BY + account_head + """ % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))), + tuple([doctype] + list(self.invoices.keys()))) + + for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: + if item_wise_tax_detail: + try: + if account in self.sa_vat_accounts: + item_wise_tax_detail = json.loads(item_wise_tax_detail) + else: + continue + for item_code, taxes in item_wise_tax_detail.items(): + is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated") + #to skip items with non-zero tax rate in multiple rows + if taxes[0] == 0 and not is_zero_rated: + continue + tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) + + if tax_rate is not None: + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \ + .setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) + except ValueError: + continue + + def get_item_amount_map(self, parent, item_code, taxes): + net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount") + tax_rate = taxes[0] + tax_amount = taxes[1] + gross_amount = net_amount + tax_amount + item_amount_map = self.item_tax_rate.setdefault(parent, {}) \ + .setdefault(item_code, []) + amount_dict = { + "tax_rate": tax_rate, + "gross_amount": gross_amount, + "tax_amount": tax_amount, + "net_amount": net_amount + } + item_amount_map.append(amount_dict) + + return tax_rate, item_amount_map + + def get_conditions(self): + conditions = "" + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if self.filters.get(opts[0]): + conditions += opts[1] + + return conditions + + def get_data(self, doctype): + consolidated_data = self.get_consolidated_data(doctype) + section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales") + + for rate, section in consolidated_data.items(): + rate = int(rate) + label = frappe.bold(section_name + "- " + "Rate" + " " + str(rate) + "%") + section_head = {"posting_date": label} + total_gross = total_tax = total_net = 0 + self.data.append(section_head) + for row in section.get("data"): + self.data.append(row) + total_gross += row["gross_amount"] + total_tax += row["tax_amount"] + total_net += row["net_amount"] + + total = { + "posting_date": frappe.bold(_("Total")), + "gross_amount": total_gross, + "tax_amount": total_tax, + "net_amount": total_net, + "bold":1 + } + self.data.append(total) + self.data.append({}) + + def get_consolidated_data(self, doctype): + consolidated_data_map={} + for inv, inv_data in self.invoices.items(): + if self.items_based_on_tax_rate.get(inv): + for rate, items in self.items_based_on_tax_rate.get(inv).items(): + consolidated_data_map.setdefault(rate, {"data": []}) + for item in items: + row = {} + item_details = self.item_tax_rate.get(inv).get(item) + row["account"] = inv_data.get("account") + row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy") + row["voucher_type"] = doctype + row["voucher_no"] = inv + row["remarks"] = inv_data.get("remarks") + row["gross_amount"]= item_details[0].get("gross_amount") + row["tax_amount"]= item_details[0].get("tax_amount") + row["net_amount"]= item_details[0].get("net_amount") + consolidated_data_map[rate]["data"].append(row) + + return consolidated_data_map + + def get_columns(self): + self.columns = [ + { + "fieldname": "posting_date", + "label": "Posting Date", + "fieldtype": "Data", + "width": 200 + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "options": "Account", + "width": 150 + }, + { + "fieldname": "voucher_type", + "label": "Voucher Type", + "fieldtype": "Data", + "width": 140, + "hidden": 1 + }, + { + "fieldname": "voucher_no", + "label": "Reference", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 150 + }, + { + "fieldname": "remarks", + "label": "Details", + "fieldtype": "Data", + "width": 150 + }, + { + "fieldname": "net_amount", + "label": "Net Amount", + "fieldtype": "Currency", + "width": 150 + }, + { + "fieldname": "tax_amount", + "label": "Tax Amount", + "fieldtype": "Currency", + "width": 150 + }, + { + "fieldname": "gross_amount", + "label": "Gross Amount", + "fieldtype": "Currency", + "width": 150 + }, + ] diff --git a/erpnext/erpnext_integrations/doctype/shopify_webhook_detail/__init__.py b/erpnext/regional/south_africa/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/shopify_webhook_detail/__init__.py rename to erpnext/regional/south_africa/__init__.py diff --git a/erpnext/regional/south_africa/setup.py b/erpnext/regional/south_africa/setup.py new file mode 100644 index 0000000000..ac783b8488 --- /dev/null +++ b/erpnext/regional/south_africa/setup.py @@ -0,0 +1,36 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +# import frappe, os, json +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.permissions import add_permission, update_permission_property + +def setup(company=None, patch=True): + add_permissions() + +def make_custom_fields(update=True): + is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', fetch_from='item_code.is_zero_rated', + insert_after='description', print_hide=1) + custom_fields = { + 'Item': [ + dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', insert_after='item_group', + print_hide=1) + ], + 'Sales Invoice Item': is_zero_rated, + 'Purchase Invoice Item': is_zero_rated + } + + create_custom_fields(custom_fields, update=update) + +def add_permissions(): + """Add Permissions for South Africa VAT Settings and South Africa VAT Account""" + for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'): + add_permission(doctype, 'All', 0) + for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + add_permission(doctype, role, 0) + update_permission_property(doctype, role, 0, 'write', 1) + update_permission_property(doctype, role, 0, 'create', 1) \ No newline at end of file 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 6e36d2809a..a4a4b0e0ed 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -564,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class { ) set_dynamic_rate_header_width(); - this.scroll_to_item($item_to_update); function set_dynamic_rate_header_width() { const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); @@ -639,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class { $($img).parent().replaceWith(`
${item_abbr}
`); } - scroll_to_item($item) { - if ($item.length === 0) return; - const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop(); - this.$cart_items_wrapper.animate({ scrollTop }); - } - update_selector_value_in_cart_item(selector, value, item) { const $item_to_update = this.get_cart_item(item); $item_to_update.attr(`data-${selector}`, escape(value)); diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index f1a166b523..63306adc6f 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -198,6 +198,7 @@ erpnext.PointOfSale.Payment = class { const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible'); this.attach_cash_shortcuts(frm.doc); !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid'); + this.render_payment_mode_dom(); }); frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index c5c01c5775..4ff2dd7e0e 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -62,12 +62,12 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling") self.assertFalse(exchange_rate == 60) - self.assertEqual(flt(exchange_rate, 3), 66.894) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict(self): # strict currency settings @@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") self.assertEqual(exchange_rate, 60.0) - # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 66.894) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling") - self.assertEqual(exchange_rate, 65.1) - - # NGN is not available on fixer.io so these should return 0 - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling") - self.assertEqual(exchange_rate, 0) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling") - self.assertEqual(exchange_rate, 0) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict_switched(self): # Start with allow_stale is True @@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase): # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 9313f95516..23e59472a6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -54,7 +54,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-08 23:13:48.049879", + "modified": "2021-08-04 20:15:59.071493", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", @@ -70,6 +70,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 691d331c74..8a49155480 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -18,7 +18,7 @@ class TransactionDeletionRecord(Document): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), + frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."), title=_("Not Allowed")) def before_submit(self): @@ -31,7 +31,7 @@ class TransactionDeletionRecord(Document): clear_notifications() self.delete_company_transactions() - def populate_doctypes_to_be_ignored_table(self): + def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: self.append('doctypes_to_be_ignored', { @@ -74,7 +74,7 @@ class TransactionDeletionRecord(Document): doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) - tables = self.get_all_child_doctypes() + tables = self.get_all_child_doctypes() for docfield in docfields: if docfield['parent'] != self.doctype: no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) @@ -90,7 +90,7 @@ class TransactionDeletionRecord(Document): naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') if naming_series: if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) + self.update_naming_series(naming_series, docfield['parent']) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') @@ -101,9 +101,9 @@ class TransactionDeletionRecord(Document): return doctypes_to_be_ignored_list def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): - docfields = frappe.get_all('DocField', + docfields = frappe.get_all('DocField', filters = { - 'fieldtype': 'Link', + 'fieldtype': 'Link', 'options': 'Company', 'parent': ['not in', doctypes_to_be_ignored_list]}, fields=['parent', 'fieldname']) @@ -121,7 +121,7 @@ class TransactionDeletionRecord(Document): self.append('doctypes', { 'doctype_name' : doctype, 'no_of_docs' : no_of_docs - }) + }) def delete_child_tables(self, doctype, company_fieldname): parent_docs_to_be_deleted = frappe.get_all(doctype, { @@ -129,7 +129,7 @@ class TransactionDeletionRecord(Document): }, pluck = 'name') child_tables = frappe.get_all('DocField', filters = { - 'fieldtype': 'Table', + 'fieldtype': 'Table', 'parent': doctype }, pluck = 'options') diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index d5dbd4cc65..e49259e1a2 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -93,21 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No try: cache = frappe.cache() - key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency) + key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) value = cache.get(key) if not value: import requests - api_url = "https://frankfurter.app/{0}".format(transaction_date) + api_url = "https://api.exchangerate.host/convert" response = requests.get(api_url, params={ - "base": from_currency, - "symbols": to_currency + "date": transaction_date, + "from": from_currency, + "to": to_currency }) # expire in 6 hours response.raise_for_status() - value = response.json()["rates"][to_currency] - - cache.set_value(key, value, expires_in_sec=6 * 60 * 60) + value = response.json()["result"] + cache.setex(name=key, time=21600, value=flt(value)) return flt(value) except: frappe.log_error(title="Get Exchange Rate") diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b6eef6ca48..b37ae3f4f6 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -162,19 +162,19 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No out = float(frappe.db.sql("""select sum(actual_qty) from `tabStock Ledger Entry` - where warehouse=%s and batch_no=%s {0}""".format(cond), + where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond), (warehouse, batch_no))[0][0] or 0) if batch_no and not warehouse: out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty from `tabStock Ledger Entry` - where batch_no=%s + where is_cancelled = 0 and batch_no=%s group by warehouse''', batch_no, as_dict=1) if not batch_no and item_code and warehouse: out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty from `tabStock Ledger Entry` - where item_code = %s and warehouse=%s + where is_cancelled = 0 and item_code = %s and warehouse=%s group by batch_no''', (item_code, warehouse), as_dict=1) return out diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 87bd9e61fe..fd080fddd9 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -138,20 +138,6 @@ frappe.ui.form.on("Item", { frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); }, - gst_hsn_code: function(frm) { - if((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { - frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { - $.each(hsn_doc.taxes || [], function(i, tax) { - let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); - a.item_tax_template = tax.item_tax_template; - a.tax_category = tax.tax_category; - a.valid_from = tax.valid_from; - frm.refresh_field('taxes'); - }); - }); - } - }, - is_fixed_asset: function(frm) { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 42cc67c5cd..614c53abb5 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -123,6 +123,7 @@ class Item(WebsiteGenerator): self.cant_change() self.update_show_in_website() self.validate_item_tax_net_rate_range() + set_item_tax_from_hsn_code(self) if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -1305,3 +1306,7 @@ def update_variants(variants, template, publish_progress=True): def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Item", ["route(500)"]) + +@erpnext.allow_regional +def set_item_tax_from_hsn_code(item): + pass \ No newline at end of file diff --git a/erpnext/stock/doctype/item/regional/india.js b/erpnext/stock/doctype/item/regional/india.js new file mode 100644 index 0000000000..77ae51fa34 --- /dev/null +++ b/erpnext/stock/doctype/item/regional/india.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Item', { + gst_hsn_code: function(frm) { + if ((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { + frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { + $.each(hsn_doc.taxes || [], function(i, tax) { + let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); + a.item_tax_template = tax.item_tax_template; + a.tax_category = tax.tax_category; + a.valid_from = tax.valid_from; + frm.refresh_field('taxes'); + }); + }); + } + }, +}); \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e795742ea4..516ae43089 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -239,6 +239,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re and sle.`item_code`=%(item_code)s and sle.`company` = %(company)s and batch.disabled = 0 + and sle.is_cancelled=0 and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 36f21465b5..6b8a410f58 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -352,7 +352,7 @@ class PurchaseReceipt(BuyingController): if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: - loss_account = self.get_company_default("default_expense_account") + loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") @@ -436,7 +436,7 @@ class PurchaseReceipt(BuyingController): "cost_center": cost_center, "debit": debit, "credit": credit, - "against_account": against_account, + "against": against_account, "remarks": remarks, } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 875814dc64..ee7fe4c9bd 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -336,10 +336,13 @@ class TestPurchaseReceipt(unittest.TestCase): se3.cancel() po.reload() pr2.load_from_db() - pr2.cancel() - po.load_from_db() - po.cancel() + if pr2.docstatus == 1 and frappe.db.get_value('Stock Ledger Entry', + {'voucher_no': pr2.name, 'is_cancelled': 0}, 'name'): + pr2.cancel() + + po.load_from_db() + po.cancel() def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) @@ -1044,7 +1047,7 @@ class TestPurchaseReceipt(unittest.TestCase): 'account': srbnb_account, 'voucher_detail_no': pr.items[1].name }, pluck="name") - + # check if the entries are not merged into one # seperate entries should be made since voucher_detail_no is different self.assertEqual(len(item_one_gl_entry), 1) @@ -1055,13 +1058,13 @@ class TestPurchaseReceipt(unittest.TestCase): def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt - + pi = create_purchase_invoice(company="_Test Company with perpetual inventory", cost_center = "Main - TCP1", warehouse = "Stores - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", currency = "USD", conversion_rate = 70) - + pr = create_purchase_receipt(pi.name) pr.conversion_rate = 80 pr.items[0].purchase_invoice = pi.name diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index b3e4286bcc..4cd40bf38e 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -29,13 +29,50 @@ frappe.ui.form.on('Repost Item Valuation', { }; }); } + + frm.trigger('setup_realtime_progress'); }, + + setup_realtime_progress: function(frm) { + frappe.realtime.on('item_reposting_progress', data => { + if (frm.doc.name !== data.name) { + return; + } + + if (frm.doc.status == 'In Progress') { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + + frm.dashboard.reset(); + frm.trigger('show_reposting_progress'); + } + }); + }, + refresh: function(frm) { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { frm.add_custom_button(__('Restart'), function () { frm.trigger("restart_reposting"); }).addClass("btn-primary"); } + + frm.trigger('show_reposting_progress'); + }, + + show_reposting_progress: function(frm) { + var bars = []; + + let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; + let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5; + var title = __('Reposting Completed {0}%', [progress]); + + bars.push({ + 'title': title, + 'width': progress + '%', + 'progress_class': 'progress-bar-success' + }); + + frm.dashboard.add_progress(__('Reposting Progress'), bars); }, restart_reposting: function(frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 071fc86d9b..a800bf8701 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -21,7 +21,10 @@ "allow_zero_rate", "amended_from", "error_section", - "error_log" + "error_log", + "items_to_be_repost", + "distinct_item_and_warehouse", + "current_index" ], "fields": [ { @@ -142,12 +145,39 @@ "fieldname": "allow_zero_rate", "fieldtype": "Check", "label": "Allow Zero Rate" + }, + { + "fieldname": "items_to_be_repost", + "fieldtype": "Code", + "hidden": 1, + "label": "Items to Be Repost", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "distinct_item_and_warehouse", + "fieldtype": "Code", + "hidden": 1, + "label": "Distinct Item and Warehouse", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "current_index", + "fieldtype": "Int", + "hidden": 1, + "label": "Current Index", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-10 07:52:12.476589", + "modified": "2021-07-22 18:59:43.057878", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 5f31d9caf0..b22759d3b7 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -81,7 +81,7 @@ def repost(doc): def repost_sl_entries(doc): if doc.based_on == 'Transaction': repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, - allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher, doc=doc) else: repost_future_sle(args=[frappe._dict({ "item_code": doc.item_code, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fcb6f0f4c2..95c7311846 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1789,7 +1789,7 @@ def get_expired_batch_items(): from `tabBatch` b, `tabStock Ledger Entry` sle where b.expiry_date <= %s and b.expiry_date is not NULL - and b.batch_id = sle.batch_no + and b.batch_id = sle.batch_no and sle.is_cancelled = 0 group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 93482e8bea..b4f458388b 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -60,7 +60,7 @@ class StockLedgerEntry(Document): if self.batch_no and not self.get("allow_negative_stock"): batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) from `tabStock Ledger Entry` - where warehouse=%s and item_code=%s and batch_no=%s""", + where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", (self.warehouse, self.item_code, self.batch_no))[0][0]) if batch_bal_after_transaction < 0: @@ -152,7 +152,7 @@ class StockLedgerEntry(Document): last_transaction_time = frappe.db.sql(""" select MAX(timestamp(posting_date, posting_time)) as posting_time from `tabStock Ledger Entry` - where docstatus = 1 and item_code = %s + where docstatus = 1 and is_cancelled = 0 and item_code = %s and warehouse = %s""", (self.item_code, self.warehouse))[0][0] cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 4540954489..84f65a077e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -56,25 +56,40 @@ frappe.ui.form.on("Stock Reconciliation", { }, get_items: function(frm) { - let fields = [{ - label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1, - "get_query": function() { - return { - "filters": { - "company": frm.doc.company, - } - }; + let fields = [ + { + label: 'Warehouse', + fieldname: 'warehouse', + fieldtype: 'Link', + options: 'Warehouse', + reqd: 1, + "get_query": function() { + return { + "filters": { + "company": frm.doc.company, + } + }; + } + }, + { + label: "Item Code", + fieldname: "item_code", + fieldtype: "Link", + options: "Item", + "get_query": function() { + return { + "filters": { + "disabled": 0, + } + }; + } + }, + { + label: __("Ignore Empty Stock"), + fieldname: "ignore_empty_stock", + fieldtype: "Check" } - }, { - label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item", - "get_query": function() { - return { - "filters": { - "disabled": 0, - } - }; - } - }]; + ]; frappe.prompt(fields, function(data) { frappe.call({ @@ -84,22 +99,21 @@ frappe.ui.form.on("Stock Reconciliation", { posting_date: frm.doc.posting_date, posting_time: frm.doc.posting_time, company: frm.doc.company, - item_code: data.item_code + item_code: data.item_code, + ignore_empty_stock: data.ignore_empty_stock }, callback: function(r) { + if (r.exc || !r.message || !r.message.length) return; + frm.clear_table("items"); - for (var i=0; i { + let item = frm.add_child("items"); + $.extend(item, row); - if (!d.valuation_rate) { - d.valuation_rate = 0; - } - } + item.qty = item.qty || 0; + item.valuation_rate = item.valuation_rate || 0; + }); frm.refresh_field("items"); } }); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9875491593..0bae7cfe25 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -483,7 +483,8 @@ class StockReconciliation(StockController): self._cancel() @frappe.whitelist() -def get_items(warehouse, posting_date, posting_time, company, item_code=None): +def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): + ignore_empty_stock = cint(ignore_empty_stock) items = [frappe._dict({ 'item_code': item_code, 'warehouse': warehouse @@ -497,18 +498,24 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None): for d in items: if d.item_code in itemwise_batch_data: - stock_bal = get_stock_balance(d.item_code, d.warehouse, - posting_date, posting_time, with_valuation_rate=True) + valuation_rate = get_stock_balance(d.item_code, d.warehouse, + posting_date, posting_time, with_valuation_rate=True)[1] for row in itemwise_batch_data.get(d.item_code): - args = get_item_data(row, row.qty, stock_bal[1]) + if ignore_empty_stock and not row.qty: + continue + + args = get_item_data(row, row.qty, valuation_rate) res.append(args) else: stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True , with_serial_no=cint(d.has_serial_no)) + qty, valuation_rate, serial_no = stock_bal[0], stock_bal[1], stock_bal[2] if cint(d.has_serial_no) else '' - args = get_item_data(d, stock_bal[0], stock_bal[1], - stock_bal[2] if cint(d.has_serial_no) else '') + if ignore_empty_stock and not stock_bal[0]: + continue + + args = get_item_data(d, qty, valuation_rate, serial_no) res.append(args) @@ -516,24 +523,44 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None): def get_items_for_stock_reco(warehouse, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - items = frappe.db.sql(""" - select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no - from tabBin bin, tabItem i - where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1 - and i.has_variants = 0 and exists( - select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse - ) - """, (lft, rgt), as_dict=1) + items = frappe.db.sql(f""" + select + i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no + from + tabBin bin, tabItem i + where + i.name = bin.item_code + and IFNULL(i.disabled, 0) = 0 + and i.is_stock_item = 1 + and i.has_variants = 0 + and exists( + select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse + ) + """, as_dict=1) items += frappe.db.sql(""" - select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no - from tabItem i, `tabItem Default` id - where i.name = id.parent - and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s + select + i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no + from + tabItem i, `tabItem Default` id + where + i.name = id.parent + and exists( + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse + ) + and i.is_stock_item = 1 + and i.has_variants = 0 + and IFNULL(i.disabled, 0) = 0 + and id.company = %s group by i.name """, (lft, rgt, company), as_dict=1) + # remove duplicates + # check if item-warehouse key extracted from each entry exists in set iw_keys + # and update iw_keys + iw_keys = set() + items = [item for item in items if [(item.item_code, item.warehouse) not in iw_keys, iw_keys.add((item.item_code, item.warehouse))][0]] + return items def get_item_data(row, qty, valuation_rate, serial_no=None): diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cf52803fca..2ed7a04ba8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -312,8 +312,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "transaction_date": args.get("transaction_date"), "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), - "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_per_unit": item.get("weight_per_unit"), + "weight_uom": item.get("weight_uom") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): diff --git a/erpnext/stock/report/cogs_by_item_group/__init__.py b/erpnext/stock/report/cogs_by_item_group/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js new file mode 100644 index 0000000000..d7c50a6697 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + + +frappe.query_reports["COGS By Item Group"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json new file mode 100644 index 0000000000..a14adf8a45 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-02 18:59:19.830928", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-02 18:59:55.470621", + "modified_by": "Administrator", + "module": "Stock", + "name": "COGS By Item Group", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "COGS By Item Group", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py new file mode 100644 index 0000000000..9e5e63e37e --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -0,0 +1,188 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict +import datetime +from typing import Dict, List, Tuple, Union + +import frappe +from frappe import _ +from frappe.utils import date_diff + +from erpnext.accounts.report.general_ledger.general_ledger import get_gl_entries + + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +DateTime = Union[datetime.date, datetime.datetime] +FilteredEntries = List[Dict[str, Union[str, float, DateTime, None]]] +ItemGroupsDict = Dict[Tuple[int, int], Dict[str, Union[str, int]]] +SVDList = List[frappe._dict] + + +def execute(filters: Filters) -> Tuple[Columns, Data]: + update_filters_with_account(filters) + validate_filters(filters) + columns = get_columns() + data = get_data(filters) + return columns, data + + +def update_filters_with_account(filters: Filters) -> None: + account = frappe.get_value("Company", filters.get("company"), "default_expense_account") + filters.update(dict(account=account)) + + +def validate_filters(filters: Filters) -> None: + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + + +def get_columns() -> Columns: + return [ + { + 'label': 'Item Group', + 'fieldname': 'item_group', + 'fieldtype': 'Data', + 'width': '200' + }, + { + 'label': 'COGS Debit', + 'fieldname': 'cogs_debit', + 'fieldtype': 'Currency', + 'width': '200' + } + ] + + +def get_data(filters: Filters) -> Data: + filtered_entries = get_filtered_entries(filters) + svd_list = get_stock_value_difference_list(filtered_entries) + leveled_dict = get_leveled_dict() + + assign_self_values(leveled_dict, svd_list) + assign_agg_values(leveled_dict) + + data = [] + for item in leveled_dict.items(): + i = item[1] + if i['agg_value'] == 0: + continue + data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level'])) + if i['self_value'] < i['agg_value'] and i['self_value'] > 0: + data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1)) + return data + + +def get_filtered_entries(filters: Filters) -> FilteredEntries: + gl_entries = get_gl_entries(filters, []) + filtered_entries = [] + for entry in gl_entries: + posting_date = entry.get('posting_date') + from_date = filters.get('from_date') + if date_diff(from_date, posting_date) > 0: + continue + filtered_entries.append(entry) + return filtered_entries + + +def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList: + voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] + svd_list = frappe.get_list( + 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], + filters=[('voucher_no', 'in', voucher_nos)] + ) + assign_item_groups_to_svd_list(svd_list) + return svd_list + + +def get_leveled_dict() -> OrderedDict: + item_groups_dict = get_item_groups_dict() + lr_list = sorted(item_groups_dict, key=lambda x : int(x[0])) + leveled_dict = OrderedDict() + current_level = 0 + nesting_r = [] + for l, r in lr_list: + while current_level > 0 and nesting_r[-1] < l: + nesting_r.pop() + current_level -= 1 + + leveled_dict[(l,r)] = { + 'level' : current_level, + 'name' : item_groups_dict[(l,r)]['name'], + 'is_group' : item_groups_dict[(l,r)]['is_group'] + } + + if int(r) - int(l) > 1: + current_level += 1 + nesting_r.append(r) + + update_leveled_dict(leveled_dict) + return leveled_dict + + +def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None: + key_dict = {v['name']:k for k, v in leveled_dict.items()} + for item in svd_list: + key = key_dict[item.get("item_group")] + leveled_dict[key]['self_value'] += -item.get("stock_value_difference") + + +def assign_agg_values(leveled_dict: OrderedDict) -> None: + keys = list(leveled_dict.keys())[::-1] + prev_level = leveled_dict[keys[-1]]['level'] + accu = [0] + for k in keys[:-1]: + curr_level = leveled_dict[k]['level'] + if curr_level == prev_level: + accu[-1] += leveled_dict[k]['self_value'] + leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value'] + + elif curr_level > prev_level: + accu.append(leveled_dict[k]['self_value']) + leveled_dict[k]['agg_value'] = accu[-1] + + elif curr_level < prev_level: + accu[-1] += leveled_dict[k]['self_value'] + leveled_dict[k]['agg_value'] = accu[-1] + + prev_level = curr_level + + # root node + rk = keys[-1] + leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value'] + + +def get_row(name:str, value:float, is_bold:int, indent:int) -> Row: + item_group = name + if is_bold: + item_group = frappe.bold(item_group) + return frappe._dict(item_group=item_group, cogs_debit=value, indent=indent) + + +def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: + ig_map = get_item_groups_map(svd_list) + for item in svd_list: + item.item_group = ig_map[item.get("item_code")] + + +def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: + item_codes = set(i['item_code'] for i in svd_list) + ig_list = frappe.get_list( + 'Item', fields=['item_code','item_group'], + filters=[('item_code', 'in', item_codes)] + ) + return {i['item_code']:i['item_group'] for i in ig_list} + + +def get_item_groups_dict() -> ItemGroupsDict: + item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) + return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']} + for i in item_groups_list} + + +def update_leveled_dict(leveled_dict: OrderedDict) -> None: + for k in leveled_dict: + leveled_dict[k].update({'self_value':0, 'agg_value':0}) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 14d543b174..bfc4471b9a 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -22,6 +22,7 @@ def get_data(report_filters): data = [] filters = { + "is_cancelled": 0, "company": report_filters.company, "posting_date": ("<=", report_filters.as_on_date) } @@ -34,7 +35,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = (d.stock_value - d.account_value) + d.difference_value = abs(d.stock_value - d.account_value) if abs(d.difference_value) > 0.1: data.append(d) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b6a8063189..9e56ad4130 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -16,8 +16,6 @@ def execute(filters=None): is_reposting_item_valuation_in_progress() if not filters: filters = {} - validate_filters(filters) - from_date = filters.get('from_date') to_date = filters.get('to_date') @@ -295,12 +293,6 @@ def get_item_reorder_details(items): return dict((d.parent + d.warehouse, d) for d in item_reorder_details) -def validate_filters(filters): - if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) - if sle_count > 500000: - frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) - def get_variants_attributes(): '''Return all item variant attributes.''' return [i.name for i in frappe.get_all('Item Attribute')] diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 5873a7a300..4108a57554 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -69,7 +69,7 @@ def get_consumed_details(filters): i.stock_uom, sle.actual_qty, sle.stock_value_difference, sle.voucher_no, sle.voucher_type from `tabStock Ledger Entry` sle, `tabItem` i - where sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): + where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): consumed_details.setdefault(d.item_code, []).append(d) return consumed_details diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c15d1eda7d..f990ce06be 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -127,30 +127,26 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.submit() return sle -def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): +def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None): if not args and voucher_type and voucher_no: - args = get_args_for_voucher(voucher_type, voucher_no) + args = get_items_to_be_repost(voucher_type, voucher_no, doc) - distinct_item_warehouses = {} - for i, d in enumerate(args): - distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ - "reposting_status": False, - "sle": d, - "args_idx": i - })) + distinct_item_warehouses = get_distinct_item_warehouse(args, doc) - i = 0 + i = get_current_index(doc) or 0 while i < len(args): + validate_item_warehouse(args[i]) + obj = update_entries_after({ - "item_code": args[i].item_code, - "warehouse": args[i].warehouse, - "posting_date": args[i].posting_date, - "posting_time": args[i].posting_time, - "creation": args[i].get("creation"), - "distinct_item_warehouses": distinct_item_warehouses + 'item_code': args[i].get('item_code'), + 'warehouse': args[i].get('warehouse'), + 'posting_date': args[i].get('posting_date'), + 'posting_time': args[i].get('posting_time'), + 'creation': args[i].get('creation'), + 'distinct_item_warehouses': distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True + distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True if obj.new_items_found: for item_wh, data in iteritems(distinct_item_warehouses): @@ -159,11 +155,41 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat args.append(data.sle) elif data.sle_changed and not data.reposting_status: args[data.args_idx] = data.sle - + data.sle_changed = False i += 1 -def get_args_for_voucher(voucher_type, voucher_no): + if doc and i % 2 == 0: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + + if doc and args: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + +def validate_item_warehouse(args): + for field in ['item_code', 'warehouse', 'posting_date', 'posting_time']: + if not args.get(field): + validation_msg = f'The field {frappe.unscrub(args.get(field))} is required for the reposting' + frappe.throw(_(validation_msg)) + +def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): + frappe.db.set_value(doc.doctype, doc.name, { + 'items_to_be_repost': json.dumps(args, default=str), + 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), + 'current_index': index + }) + + frappe.db.commit() + + frappe.publish_realtime('item_reposting_progress', { + 'name': doc.name, + 'items_to_be_repost': json.dumps(args, default=str), + 'current_index': index + }) + +def get_items_to_be_repost(voucher_type, voucher_no, doc=None): + if doc and doc.items_to_be_repost: + return json.loads(doc.items_to_be_repost) or [] + return frappe.db.get_all("Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], @@ -171,6 +197,25 @@ def get_args_for_voucher(voucher_type, voucher_no): group_by="item_code, warehouse" ) +def get_distinct_item_warehouse(args=None, doc=None): + distinct_item_warehouses = {} + if doc and doc.distinct_item_and_warehouse: + distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) + distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + else: + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) + + return distinct_item_warehouses + +def get_current_index(doc=None): + if doc and doc.current_index: + return doc.current_index + class update_entries_after(object): """ update valution rate and qty after transaction