Merge pull request #28764 from deepeshgarg007/tds_report_fixes
fix: TDS Monthly payable report
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index c145291..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-name: Bug report
-about: Report a bug encountered while using ERPNext
-labels: bug
----
-
-<!--
-Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
-
-1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- - For documentation issues, refer to https://github.com/frappe/erpnext_com
-2. Use the search function before creating a new issue. Duplicates will be closed and directed to
- the original discussion.
-3. When making a bug report, make sure you provide all required information. The easier it is for
- maintainers to reproduce, the faster it'll be fixed.
-4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
--->
-
-## Description of the issue
-
-## Context information (for bug reports)
-
-**Output of `bench version`**
-```
-(paste here)
-```
-
-## Steps to reproduce the issue
-
-1.
-2.
-3.
-
-### Observed result
-
-### Expected result
-
-### Stacktrace / full error message
-
-```
-(paste here)
-```
-
-## Additional information
-
-OS version / distribution, `ERPNext` install method, etc.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000..a6e16a0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,106 @@
+name: Bug Report
+description: Report a bug encountered while using ERPNext
+labels: ["bug"]
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
+
+ 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
+ - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
+ - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
+ 2. When making a bug report, make sure you provide all required information. The easier it is for
+ maintainers to reproduce, the faster it'll be fixed.
+ 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
+
+ - type: textarea
+ id: bug-info
+ attributes:
+ label: Information about bug
+ description: Also tell us, what did you expect to happen?
+ placeholder: Please provide as much information as possible.
+ validations:
+ required: true
+
+ - type: dropdown
+ id: version
+ attributes:
+ label: Version
+ description: Affected versions.
+ multiple: true
+ options:
+ - v12
+ - v13
+ - v14
+ - develop
+ validations:
+ required: true
+
+ - type: dropdown
+ id: module
+ attributes:
+ label: Module
+ description: Select affected module of ERPNext.
+ multiple: true
+ options:
+ - accounts
+ - stock
+ - buying
+ - selling
+ - ecommerce
+ - manufacturing
+ - HR
+ - projects
+ - support
+ - assets
+ - integrations
+ - quality
+ - regional
+ - portal
+ - agriculture
+ - education
+ - non-profit
+ validations:
+ required: true
+
+ - type: textarea
+ id: exact-version
+ attributes:
+ label: Version
+ description: Share exact version number of Frappe and ERPNext you are using.
+ placeholder: |
+ Frappe version -
+ ERPNext Verion -
+ validations:
+ required: true
+
+ - type: dropdown
+ id: install-method
+ attributes:
+ label: Installation method
+ options:
+ - docker
+ - easy-install
+ - manual install
+ - FrappeCloud
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Relevant log output / Stack trace / Full Error Message.
+ description: Please copy and paste any relevant log output. This will be automatically formatted.
+ render: shell
+
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 6cdad35..418bf3c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
+title: ''
labels: feature-request
+assignees: ''
+
---
<!--
diff --git a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md b/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
deleted file mode 100644
index 2016bcc..0000000
--- a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Question about using ERPNext
-about: This is not the appropriate channel
-labels: invalid
----
-
-Please post on our forums:
-
-for questions about using `ERPNext`: https://discuss.erpnext.com
-
-for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
-
-for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
-
-For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
-
-> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**
\ No newline at end of file
diff --git a/.github/stale.yml b/.github/stale.yml
index 9322ae8..8b7cb9b 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 15
+# Label to use when marking as stale
+staleLabel: inactive
-# Number of days of inactivity before a stale Issue or Pull Request is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 3
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
- - hotfix
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 10
# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
+exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
-# Label to use when marking as stale
-staleLabel: inactive
+pulls:
+ daysUntilStale: 15
+ daysUntilClose: 3
+ exemptLabels:
+ - hotfix
+ markComment: >
+ This pull request has been automatically marked as inactive because it has
+ not had recent activity. It will be closed within 3 days if no further
+ activity occurs, but it only takes a comment to keep a contribution alive
+ :) Also, even if it is closed, you can always reopen the PR when you're
+ ready. Thank you for contributing.
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
- This pull request has been automatically marked as stale because it has not had
- recent activity. It will be closed within a week if no further activity occurs, but it
- only takes a comment to keep a contribution alive :) Also, even if it is closed,
- you can always reopen the PR when you're ready. Thank you for contributing.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 30
-
-# Limit to only `issues` or `pulls`
-only: pulls
+issues:
+ daysUntilStale: 60
+ daysUntilClose: 7
+ exemptLabels:
+ - valid
+ - to-validate
+ markComment: >
+ This issue has been automatically marked as inactive because it has not had
+ recent activity and it wasn't validated by maintainer team. It will be
+ closed within a week if no further activity occurs.
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 5df8269..22c81dd 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -374,12 +374,13 @@
frappe.db.commit()
except Exception as e:
if frappe.flags.in_test:
+ traceback = frappe.get_traceback()
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
-
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
@@ -446,10 +447,12 @@
if submit:
journal_entry.submit()
+
+ frappe.db.commit()
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 7451917..4839207 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -10,6 +10,8 @@
from frappe.model.document import Document
from frappe.utils import cint
+from erpnext.stock.utils import check_pending_reposting
+
class AccountsSettings(Document):
def on_update(self):
@@ -25,6 +27,7 @@
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
+ self.validate_pending_reposts()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -56,3 +59,8 @@
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+
+
+ def validate_pending_reposts(self):
+ if self.acc_frozen_upto:
+ check_pending_reposting(self.acc_frozen_upto)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 48b5cb9..f4fd1bf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -114,6 +114,9 @@
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index c4d59f1..64712b5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -155,6 +155,8 @@
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e904768..6b4b43d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -68,10 +68,12 @@
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address)
- if not party_details.get("taxes_and_charges"):
- party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
- customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
- billing_address=party_address, shipping_address=shipping_address)
+ tax_template = set_taxes(party.name, party_type, posting_date, company,
+ customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
+ billing_address=party_address, shipping_address=shipping_address)
+
+ if tax_template:
+ party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index ca10b1d..874fb63 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -57,8 +57,10 @@
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
+ credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
+
credit_entry = {
- "account": accumulated_depreciation_account,
+ "account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -66,7 +68,7 @@
}
debit_entry = {
- "account": depreciation_expense_account,
+ "account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -132,6 +134,20 @@
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
+def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
+ root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
+
+ if root_type == "Expense":
+ credit_account = accumulated_depreciation_account
+ debit_account = depreciation_expense_account
+ elif root_type == "Income":
+ credit_account = depreciation_expense_account
+ debit_account = accumulated_depreciation_account
+ else:
+ frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
+
+ return credit_account, debit_account
+
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 0e8ceb5..ce2cb01 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -868,6 +868,72 @@
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry)
+ def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
+ """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+ else:
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+
+ def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
+ """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
+
+ depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+ depr_expense_account.root_type = "Income"
+ depr_expense_account.parent_account = "Income - _TC"
+ depr_expense_account.save()
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+ else:
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+
+ # resetting
+ depr_expense_account.root_type = "Expense"
+ depr_expense_account.parent_account = "Expenses - _TC"
+ depr_expense_account.save()
+
def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected."""
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index 51ce157..c702687 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,7 +33,7 @@
var d = locals[cdt][cdn];
return {
"filters": {
- "root_type": "Expense",
+ "root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name
}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index e2f3ca3..bd573bf 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -42,10 +42,10 @@
def validate_account_types(self):
account_type_map = {
- 'fixed_asset_account': { 'account_type': 'Fixed Asset' },
- 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
- 'depreciation_expense_account': { 'root_type': 'Expense' },
- 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
+ 'fixed_asset_account': {'account_type': ['Fixed Asset']},
+ 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
+ 'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
+ 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
}
for d in self.accounts:
for fieldname in account_type_map.keys():
@@ -53,11 +53,11 @@
selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
- expected_key_type = account_type_map[fieldname][key_to_match]
+ expected_key_types = account_type_map[fieldname][key_to_match]
- if selected_key_type != expected_key_type:
+ if selected_key_type not in expected_key_types:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
- .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
+ .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account"))
def valide_cwip_account(self):
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5eab21b..1b5f35e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -72,6 +72,7 @@
self.create_raw_materials_supplied("supplied_items")
self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(PurchaseOrder, self).validate_with_previous_doc({
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 0a51a8e..023c95d 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -17,6 +17,7 @@
"company",
"transaction_date",
"valid_till",
+ "quotation_number",
"amended_from",
"address_section",
"supplier_address",
@@ -797,6 +798,11 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till"
+ },
+ {
+ "fieldname": "quotation_number",
+ "fieldtype": "Data",
+ "label": "Quotation Number"
}
],
"icon": "fa fa-shopping-cart",
@@ -804,10 +810,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 00:58:20.995491",
+ "modified": "2021-12-11 06:43:20.924080",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py
new file mode 100644
index 0000000..13aa697
--- /dev/null
+++ b/erpnext/controllers/tests/test_transaction_base.py
@@ -0,0 +1,22 @@
+import unittest
+
+import frappe
+
+
+class TestUtils(unittest.TestCase):
+ def test_reset_default_field_value(self):
+ doc = frappe.get_doc({
+ "doctype": "Purchase Receipt",
+ "set_warehouse": "Warehouse 1",
+ })
+
+ # Same values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, "Warehouse 1")
+
+ # Mixed values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, None)
+
diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/__init__.py
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js
new file mode 100644
index 0000000..c6569d8
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('CRM Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
new file mode 100644
index 0000000..95b19fa
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -0,0 +1,114 @@
+{
+ "actions": [],
+ "creation": "2021-09-09 17:03:22.754446",
+ "description": "Settings for Selling Module",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_5",
+ "campaign_naming_by",
+ "allow_lead_duplication_based_on_emails",
+ "column_break_4",
+ "create_event_on_next_contact_date",
+ "auto_creation_of_contact",
+ "opportunity_section",
+ "close_opportunity_after_days",
+ "column_break_9",
+ "create_event_on_next_contact_date_opportunity",
+ "quotation_section",
+ "default_valid_till"
+ ],
+ "fields": [
+ {
+ "fieldname": "campaign_naming_by",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Campaign Naming By",
+ "options": "Campaign Name\nNaming Series"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_valid_till",
+ "fieldtype": "Data",
+ "label": "Default Quotation Validity Days"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Lead"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_lead_duplication_based_on_emails",
+ "fieldtype": "Check",
+ "label": "Allow Lead Duplication based on Emails"
+ },
+ {
+ "default": "1",
+ "fieldname": "auto_creation_of_contact",
+ "fieldtype": "Check",
+ "label": "Auto Creation of Contact"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "opportunity_section",
+ "fieldtype": "Section Break",
+ "label": "Opportunity"
+ },
+ {
+ "default": "15",
+ "description": "Auto close Opportunity Replied after the no. of days mentioned above",
+ "fieldname": "close_opportunity_after_days",
+ "fieldtype": "Int",
+ "label": "Close Replied Opportunity After Days"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date_opportunity",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "quotation_section",
+ "fieldtype": "Section Break",
+ "label": "Quotation"
+ }
+ ],
+ "icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
+ "modified": "2021-11-03 09:00:36.883496",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py
new file mode 100644
index 0000000..bde5254
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMSettings(Document):
+ pass
diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
new file mode 100644
index 0000000..3372c5d
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestCRMSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c590523..9adbe8b 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -11,6 +11,7 @@
cint,
comma_and,
cstr,
+ get_link_to_form,
getdate,
has_gravatar,
nowdate,
@@ -91,13 +92,14 @@
self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False):
- super(Lead, self).add_calendar_event({
- "owner": self.lead_owner,
- "starts_on": self.contact_date,
- "ends_on": self.ends_on or "",
- "subject": ('Contact ' + cstr(self.lead_name)),
- "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
- }, force)
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
+ super(Lead, self).add_calendar_event({
+ "owner": self.lead_owner,
+ "starts_on": self.contact_date,
+ "ends_on": self.ends_on or "",
+ "subject": ('Contact ' + cstr(self.lead_name)),
+ "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
+ }, force)
def update_prospects(self):
prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
@@ -108,12 +110,13 @@
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
- duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
- duplicate_leads = [lead.name for lead in duplicate_leads]
+ if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
+ duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
+ duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
- if duplicate_leads:
- frappe.throw(_("Email Address must be unique, already exists for {0}")
- .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
+ if duplicate_leads:
+ frappe.throw(_("Email Address must be unique, already exists for {0}")
+ .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
@@ -172,41 +175,42 @@
self.title = self.company_name or self.lead_name
def create_contact(self):
- if not self.lead_name:
- self.set_full_name()
- self.set_lead_name()
+ if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
+ if not self.lead_name:
+ self.set_full_name()
+ self.set_lead_name()
- contact = frappe.new_doc("Contact")
- contact.update({
- "first_name": self.first_name or self.lead_name,
- "last_name": self.last_name,
- "salutation": self.salutation,
- "gender": self.gender,
- "designation": self.designation,
- "company_name": self.company_name,
- })
-
- if self.email_id:
- contact.append("email_ids", {
- "email_id": self.email_id,
- "is_primary": 1
+ contact = frappe.new_doc("Contact")
+ contact.update({
+ "first_name": self.first_name or self.lead_name,
+ "last_name": self.last_name,
+ "salutation": self.salutation,
+ "gender": self.gender,
+ "designation": self.designation,
+ "company_name": self.company_name,
})
- if self.phone:
- contact.append("phone_nos", {
- "phone": self.phone,
- "is_primary_phone": 1
- })
+ if self.email_id:
+ contact.append("email_ids", {
+ "email_id": self.email_id,
+ "is_primary": 1
+ })
- if self.mobile_no:
- contact.append("phone_nos", {
- "phone": self.mobile_no,
- "is_primary_mobile_no":1
- })
+ if self.phone:
+ contact.append("phone_nos", {
+ "phone": self.phone,
+ "is_primary_phone": 1
+ })
- contact.insert(ignore_permissions=True)
+ if self.mobile_no:
+ contact.append("phone_nos", {
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
+ })
- return contact
+ contact.insert(ignore_permissions=True)
+
+ return contact
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 0bef80a..fcbd4de 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -8,6 +8,7 @@
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname
from erpnext.setup.utils import get_exchange_rate
@@ -28,7 +29,6 @@
})
self.make_new_lead_if_required()
-
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
@@ -70,21 +70,21 @@
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
# check if customer is already created agains the self.contact_email
- customer = frappe.db.sql("""select
- distinct `tabDynamic Link`.link_name as customer
- from
- `tabContact`,
- `tabDynamic Link`
- where `tabContact`.email_id='{0}'
- and
- `tabContact`.name=`tabDynamic Link`.parent
- and
- ifnull(`tabDynamic Link`.link_name, '')<>''
- and
- `tabDynamic Link`.link_doctype='Customer'
- """.format(self.contact_email), as_dict=True)
- if customer and customer[0].customer:
- self.party_name = customer[0].customer
+ dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
+ customer = frappe.qb.from_(
+ dynamic_link
+ ).join(
+ contact
+ ).on(
+ (contact.name == dynamic_link.parent)
+ & (dynamic_link.link_doctype == "Customer")
+ & (contact.email_id == self.contact_email)
+ ).select(
+ dynamic_link.link_name
+ ).distinct().run(as_dict=True)
+
+ if customer and customer[0].link_name:
+ self.party_name = customer[0].link_name
self.opportunity_from = "Customer"
return
@@ -191,30 +191,31 @@
self.add_calendar_event()
def add_calendar_event(self, opts=None, force=False):
- if not opts:
- opts = frappe._dict()
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
+ if not opts:
+ opts = frappe._dict()
- opts.description = ""
- opts.contact_date = self.contact_date
+ opts.description = ""
+ opts.contact_date = self.contact_date
- if self.party_name and self.opportunity_from == 'Customer':
- if self.contact_person:
- opts.description = 'Contact '+cstr(self.contact_person)
- else:
- opts.description = 'Contact customer '+cstr(self.party_name)
- elif self.party_name and self.opportunity_from == 'Lead':
- if self.contact_display:
- opts.description = 'Contact '+cstr(self.contact_display)
- else:
- opts.description = 'Contact lead '+cstr(self.party_name)
+ if self.party_name and self.opportunity_from == 'Customer':
+ if self.contact_person:
+ opts.description = 'Contact '+cstr(self.contact_person)
+ else:
+ opts.description = 'Contact customer '+cstr(self.party_name)
+ elif self.party_name and self.opportunity_from == 'Lead':
+ if self.contact_display:
+ opts.description = 'Contact '+cstr(self.contact_display)
+ else:
+ opts.description = 'Contact lead '+cstr(self.party_name)
- opts.subject = opts.description
- opts.description += '. By : ' + cstr(self.contact_by)
+ opts.subject = opts.description
+ opts.description += '. By : ' + cstr(self.contact_by)
- if self.to_discuss:
- opts.description += ' To Discuss : ' + cstr(self.to_discuss)
+ if self.to_discuss:
+ opts.description += ' To Discuss : ' + cstr(self.to_discuss)
- super(Opportunity, self).add_calendar_event(opts, force)
+ super(Opportunity, self).add_calendar_event(opts, force)
def validate_item_details(self):
if not self.get('items'):
@@ -363,7 +364,7 @@
def auto_close_opportunity():
""" auto close the `Replied` Opportunities after 7 days """
- auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15
+ auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index 23ccb7e..ae1f36e 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -20,7 +20,6 @@
"configuration_cb",
"shipping_account_head",
"section_break_12",
- "nexus_address",
"nexus"
],
"fields": [
@@ -87,16 +86,12 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "nexus",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Nexus List"
},
{
- "fieldname": "nexus_address",
- "fieldtype": "HTML",
- "label": "Nexus Address"
- },
- {
"fieldname": "nexus",
"fieldtype": "Table",
"label": "Nexus",
@@ -108,19 +103,20 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
}
],
"issingle": 1,
"links": [],
- "modified": "2021-11-08 18:02:29.232090",
+ "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
+ "modified": "2021-11-30 11:17:24.647979",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index b9f24b6..d4bbe88 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -16,9 +16,9 @@
class TaxJarSettings(Document):
def on_update(self):
- TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
- TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
- TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+ TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
+ TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
+ TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 05c46c5..63530ea 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -265,6 +265,9 @@
"erpnext.regional.india.utils.update_taxable_values"
]
},
+ "POS Invoice": {
+ "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
+ },
"Purchase Invoice": {
"validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index db126cd..2ffae1a 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -323,10 +323,14 @@
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
- def update_sales(source, target, parent):
+ def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
- target.serial_no = ''
+ serial_nos = get_serial_nos(target.serial_no)
+ if len(serial_nos) == 1:
+ target.serial_no = serial_nos[0]
+ else:
+ target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": {
@@ -342,7 +346,7 @@
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name,
- "postprocess": update_sales
+ "postprocess": update_sales_and_serial
}
}, target_doc)
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index 7f98354..6f6ca61 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -43,14 +43,11 @@
}
});
}
- else {
- frm.clear_table("purposes");
- }
-
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
+ frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
index acaa895..2c48872 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
@@ -4,10 +4,17 @@
def get_data():
return {
'fieldname': 'job_card',
+ 'non_standard_fieldnames': {
+ 'Quality Inspection': 'reference_name'
+ },
'transactions': [
{
'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry']
+ },
+ {
+ 'label': _('Reference'),
+ 'items': ['Quality Inspection']
}
]
}
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 897e70c..26fb859 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -278,6 +278,7 @@
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
+erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
@@ -312,3 +313,6 @@
erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
+erpnext.patches.v14_0.migrate_crm_settings
+erpnext.patches.v13_0.rename_ksa_qr_field
+erpnext.patches.v13_0.disable_ksa_print_format_for_others
diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py
index b3ef5af..5e1df14 100644
--- a/erpnext/patches/v13_0/check_is_income_tax_component.py
+++ b/erpnext/patches/v13_0/check_is_income_tax_component.py
@@ -3,9 +3,9 @@
import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
import erpnext
-from erpnext.regional.india.setup import setup
def execute():
@@ -30,7 +30,14 @@
frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India":
- setup(patch=True)
+ create_custom_field('Salary Component',
+ dict(fieldname='component_type',
+ label='Component Type',
+ fieldtype='Select',
+ insert_after='description',
+ options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
+ depends_on='eval:doc.type == "Deduction"')
+ )
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
new file mode 100644
index 0000000..c815b3b
--- /dev/null
+++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+ if company:
+ return
+
+ if frappe.db.exists('DocType', 'Print Format'):
+ frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+ for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
+ frappe.db.set_value("Print Format", d, "disabled", 1)
diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
new file mode 100644
index 0000000..5487a6c
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -0,0 +1,15 @@
+import frappe
+from frappe import _
+
+
+def execute():
+ selling_settings = frappe.get_single("Selling Settings")
+
+ if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
+ selling_settings.customer_group = None
+
+ if selling_settings.territory in (_("All Territories"), "All Territories"):
+ selling_settings.territory = None
+
+ selling_settings.flags.ignore_mandatory=True
+ selling_settings.save(ignore_permissions=True)
diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py
new file mode 100644
index 0000000..0bb86e0
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+ if not company:
+ return
+
+ if frappe.db.exists('DocType', 'Sales Invoice'):
+ frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
+ if frappe.db.has_column('Sales Invoice', 'qr_code'):
+ rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
new file mode 100644
index 0000000..30d3ea0
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
+ 'campaign_naming_by',
+ 'close_opportunity_after_days',
+ 'default_valid_till'
+ ], as_dict=True)
+
+ frappe.reload_doc('crm', 'doctype', 'crm_settings')
+ frappe.db.set_value('CRM Settings', 'CRM Settings', {
+ 'campaign_naming_by': settings.campaign_naming_by,
+ 'close_opportunity_after_days': settings.close_opportunity_after_days,
+ 'default_valid_till': settings.default_valid_till
+ })
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f0facdd..cad1659 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -84,6 +84,10 @@
});
},
+ route_to_pending_reposts: (args) => {
+ frappe.set_route('List', 'Repost Item Valuation', args);
+ },
+
proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog();
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 12d8d20..fd3ec3c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -206,26 +206,17 @@
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
-
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.company_gstin:
- return party_details
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.supplier_gstin:
- return party_details
+ if tax_template_by_category:
+ party_details['taxes_and_charges'] = tax_template_by_category
+ return
if not party_details.place_of_supply: return party_details
-
if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
@@ -237,6 +228,7 @@
if not default_tax:
return party_details
+
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
@@ -268,9 +260,7 @@
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
'name')
- if default_tax:
- party_details["taxes_and_charges"] = default_tax
- party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
+ return default_tax
def get_tax_template(master_doctype, company, is_inter_state, state_code):
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
new file mode 100644
index 0000000..c2a3092
--- /dev/null
+++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
@@ -0,0 +1,32 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-12-07 13:25:05.424827",
+ "css": "",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 1,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font_size": 0,
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 0,
+ "line_breaks": 0,
+ "margin_bottom": 0.0,
+ "margin_left": 0.0,
+ "margin_right": 0.0,
+ "margin_top": 0.0,
+ "modified": "2021-12-08 10:25:01.930885",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA POS Invoice",
+ "owner": "Administrator",
+ "page_number": "Hide",
+ "print_format_builder": 0,
+ "print_format_builder_beta": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
index 8e9a728..6b64d47 100644
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
@@ -5,19 +5,19 @@
"css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
"custom_format": 1,
"default_print_language": "en",
- "disabled": 0,
+ "disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
+ "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.ksa_einv_qr}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2021-11-29 13:47:37.870818",
+ "modified": "2021-12-07 13:43:38.018593",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Invoice",
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 38a089c..2e31c03 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -3,7 +3,7 @@
import frappe
from frappe.permissions import add_permission, update_permission_property
-from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -13,6 +13,16 @@
add_permissions()
make_custom_fields()
+def add_print_formats():
+ frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+
+ for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
+ frappe.db.set_value("Print Format", d, "disabled", 0)
+
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0)
@@ -33,8 +43,16 @@
custom_fields = {
'Sales Invoice': [
dict(
- fieldname='qr_code',
- label='QR Code',
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1
+ )
+ ],
+ 'POS Invoice': [
+ dict(
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index ba55efc..a03c3f0 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -1,147 +1,149 @@
import io
import os
from base64 import b64encode
-from urllib.parse import quote
import frappe
from frappe import _
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
-def create_qr_code(doc, method):
- """Create QR Code after inserting Sales Inv
- """
-
+def create_qr_code(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
- # if QR Code field not present, do nothing
- if not hasattr(doc, 'qr_code'):
- return
+ # if QR Code field not present, create it. Invoices without QR are invalid as per law.
+ if not hasattr(doc, 'ksa_einv_qr'):
+ create_custom_fields({
+ doc.doctype: [
+ dict(
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1
+ )
+ ]
+ })
# Don't create QR Code if it already exists
- qr_code = doc.get("qr_code")
+ qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
- meta = frappe.get_meta('Sales Invoice')
+ meta = frappe.get_meta(doc.doctype)
- for field in meta.get_image_fields():
- if field.fieldname == 'qr_code':
- ''' TLV conversion for
- 1. Seller's Name
- 2. VAT Number
- 3. Time Stamp
- 4. Invoice Amount
- 5. VAT Amount
- '''
- tlv_array = []
- # Sellers Name
+ if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
+ ''' TLV conversion for
+ 1. Seller's Name
+ 2. VAT Number
+ 3. Time Stamp
+ 4. Invoice Amount
+ 5. VAT Amount
+ '''
+ tlv_array = []
+ # Sellers Name
- seller_name = frappe.db.get_value(
- 'Company',
- doc.company,
- 'company_name_in_arabic')
+ seller_name = frappe.db.get_value(
+ 'Company',
+ doc.company,
+ 'company_name_in_arabic')
- if not seller_name:
- frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
+ if not seller_name:
+ frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
- tag = bytes([1]).hex()
- length = bytes([len(seller_name.encode('utf-8'))]).hex()
- value = seller_name.encode('utf-8').hex()
- tlv_array.append(''.join([tag, length, value]))
+ tag = bytes([1]).hex()
+ length = bytes([len(seller_name.encode('utf-8'))]).hex()
+ value = seller_name.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # VAT Number
- tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
- if not tax_id:
- frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
+ # VAT Number
+ tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
+ if not tax_id:
+ frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
- tag = bytes([2]).hex()
- length = bytes([len(tax_id)]).hex()
- value = tax_id.encode('utf-8').hex()
- tlv_array.append(''.join([tag, length, value]))
+ tag = bytes([2]).hex()
+ length = bytes([len(tax_id)]).hex()
+ value = tax_id.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # Time Stamp
- posting_date = getdate(doc.posting_date)
- time = get_time(doc.posting_time)
- seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
- time_stamp = add_to_date(posting_date, seconds=seconds)
- time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
+ # Time Stamp
+ posting_date = getdate(doc.posting_date)
+ time = get_time(doc.posting_time)
+ seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
+ time_stamp = add_to_date(posting_date, seconds=seconds)
+ time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
- tag = bytes([3]).hex()
- length = bytes([len(time_stamp)]).hex()
- value = time_stamp.encode('utf-8').hex()
- tlv_array.append(''.join([tag, length, value]))
+ tag = bytes([3]).hex()
+ length = bytes([len(time_stamp)]).hex()
+ value = time_stamp.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # Invoice Amount
- invoice_amount = str(doc.total)
- tag = bytes([4]).hex()
- length = bytes([len(invoice_amount)]).hex()
- value = invoice_amount.encode('utf-8').hex()
- tlv_array.append(''.join([tag, length, value]))
+ # Invoice Amount
+ invoice_amount = str(doc.grand_total)
+ tag = bytes([4]).hex()
+ length = bytes([len(invoice_amount)]).hex()
+ value = invoice_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # VAT Amount
- vat_amount = str(doc.total_taxes_and_charges)
+ # VAT Amount
+ vat_amount = str(doc.total_taxes_and_charges)
- tag = bytes([5]).hex()
- length = bytes([len(vat_amount)]).hex()
- value = vat_amount.encode('utf-8').hex()
- tlv_array.append(''.join([tag, length, value]))
+ tag = bytes([5]).hex()
+ length = bytes([len(vat_amount)]).hex()
+ value = vat_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # Joining bytes into one
- tlv_buff = ''.join(tlv_array)
+ # Joining bytes into one
+ tlv_buff = ''.join(tlv_array)
- # base64 conversion for QR Code
- base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
+ # base64 conversion for QR Code
+ base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
- qr_image = io.BytesIO()
- url = qr_create(base64_string, error='L')
- url.png(qr_image, scale=2, quiet_zone=1)
+ qr_image = io.BytesIO()
+ url = qr_create(base64_string, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
- urlencoded_name = quote(doc.name)
- # making file
- filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__")
- _file = frappe.get_doc({
- "doctype": "File",
- "file_name": filename,
- "is_private": 0,
- "content": qr_image.getvalue(),
- "attached_to_doctype": doc.get("doctype"),
- "attached_to_name": doc.get("name"),
- "attached_to_field": "qr_code"
- })
+ name = frappe.generate_hash(doc.name, 5)
- _file.save()
+ # making file
+ filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "is_private": 0,
+ "content": qr_image.getvalue(),
+ "attached_to_doctype": doc.get("doctype"),
+ "attached_to_name": doc.get("name"),
+ "attached_to_field": "ksa_einv_qr"
+ })
- # assigning to document
- doc.db_set('qr_code', _file.file_url)
- doc.notify_update()
+ _file.save()
- break
+ # assigning to document
+ doc.db_set('ksa_einv_qr', _file.file_url)
+ doc.notify_update()
-def delete_qr_code_file(doc, method):
- """Delete QR Code on deleted sales invoice"""
-
+def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
- if hasattr(doc, 'qr_code'):
- if doc.get('qr_code'):
+ if hasattr(doc, 'ksa_einv_qr'):
+ if doc.get('ksa_einv_qr'):
file_doc = frappe.get_list('File', {
- 'file_url': doc.get('qr_code')
+ 'file_url': doc.get('ksa_einv_qr')
})
if len(file_doc):
frappe.delete_doc('File', file_doc[0].name)
-def delete_vat_settings_for_company(doc, method):
+def delete_vat_settings_for_company(doc, method=None):
if doc.country != 'Saudi Arabia':
return
- settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
- settings_doc.delete()
\ No newline at end of file
+ if frappe.db.exists('KSA VAT Setting', doc.name):
+ frappe.delete_doc('KSA VAT Setting', doc.name)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 1c24825..cc95185 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -63,6 +63,8 @@
if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered'
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
def validate_po(self):
# validate p.o date v/s delivery date
if self.po_date and not self.skip_delivery_note:
@@ -978,6 +980,7 @@
description=i['description']
)).insert()
work_order.set_work_order_operations()
+ work_order.flags.ignore_mandatory = True
work_order.save()
out.append(work_order)
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index c27f1ea..27bc541 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -11,11 +11,6 @@
"customer_group",
"column_break_4",
"territory",
- "crm_settings_section",
- "campaign_naming_by",
- "default_valid_till",
- "column_break_9",
- "close_opportunity_after_days",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
@@ -44,13 +39,6 @@
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
- "fieldname": "campaign_naming_by",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Campaign Naming By",
- "options": "Campaign Name\nNaming Series\nAuto Name"
- },
- {
"fieldname": "customer_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -72,18 +60,6 @@
"options": "Price List"
},
{
- "default": "15",
- "description": "Auto close Opportunity after the no. of days mentioned above",
- "fieldname": "close_opportunity_after_days",
- "fieldtype": "Int",
- "label": "Close Opportunity After Days"
- },
- {
- "fieldname": "default_valid_till",
- "fieldtype": "Data",
- "label": "Default Quotation Validity Days"
- },
- {
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
@@ -170,15 +146,6 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "crm_settings_section",
- "fieldtype": "Section Break",
- "label": "CRM Settings"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
@@ -204,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-08 19:38:10.175989",
+ "modified": "2021-09-13 12:32:17.004404",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index e7c5e76..fb86e61 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -8,7 +8,6 @@
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
-from frappe.utils.nestedset import get_root_of
class SellingSettings(Document):
@@ -37,9 +36,3 @@
editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
-
- def set_default_customer_group_and_territory(self):
- if not self.customer_group:
- self.customer_group = get_root_of('Customer Group')
- if not self.territory:
- self.territory = get_root_of('Territory')
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 503aeac..98f9119 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -303,7 +303,6 @@
def update_selling_defaults():
selling_settings = frappe.get_doc("Selling Settings")
- selling_settings.set_default_customer_group_and_territory()
selling_settings.cust_master_name = "Customer Name"
selling_settings.so_required = "No"
selling_settings.dn_required = "No"
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 1478007..cad4c54 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -53,6 +53,7 @@
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains()
+ set_defaults_for_tests()
frappe.db.commit()
@@ -127,6 +128,14 @@
[d.name for d in domains])
add_all_roles_to('Administrator')
+def set_defaults_for_tests():
+ from frappe.utils.nestedset import get_root_of
+
+ selling_settings = frappe.get_single("Selling Settings")
+ selling_settings.customer_group = get_root_of("Customer Group")
+ selling_settings.territory = get_root_of("Territory")
+ selling_settings.save()
+
def insert_record(records):
for r in records:
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 1412acf..e47837f 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -10,7 +10,7 @@
"idx": 0,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-10-26 21:32:55.323591",
+ "modified": "2021-11-05 21:32:55.323591",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -123,6 +123,13 @@
"label": "Products Settings",
"link_to": "Products Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "crm",
+ "label": "CRM Settings",
+ "link_to": "CRM Settings",
+ "type": "DocType"
}
],
"title": "ERPNext Settings"
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index ed8c878..0da45a5 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -22,7 +22,7 @@
'customer_group')
bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings',
'allow_stale'))
- bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings',
+ bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings',
'default_valid_till'))
# if no company, show a dialog box to create a new company
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index da9c66d..a33134b 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -6,7 +6,7 @@
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import flt, nowdate
+from frappe.utils import flt
class Bin(Document):
@@ -127,33 +127,11 @@
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
- '''Called from erpnext.stock.utils.update_bin'''
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
+ from erpnext.stock.stock_ledger import repost_current_voucher
+
update_qty(bin_name, args)
-
- if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
- if not args.get("posting_date"):
- args["posting_date"] = nowdate()
-
- if args.get("is_cancelled") and via_landed_cost_voucher:
- return
-
- # Reposts only current voucher SL Entries
- # Updates valuation rate, stock value, stock queue for current transaction
- update_entries_after({
- "item_code": args.get('item_code'),
- "warehouse": args.get('warehouse'),
- "posting_date": args.get("posting_date"),
- "posting_time": args.get("posting_time"),
- "voucher_type": args.get("voucher_type"),
- "voucher_no": args.get("voucher_no"),
- "sle_id": args.get('name'),
- "creation": args.get('creation')
- }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
- # update qty in future sle and Validate negative qty
- update_qty_in_future_sle(args, allow_negative_stock)
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 5268460..70d48a4 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -138,6 +138,7 @@
self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed'
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(DeliveryNote, self).validate_with_previous_doc({
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 5469a9f..4f4e691 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -1035,7 +1035,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-11-30 02:33:06.572442",
+ "modified": "2021-12-03 08:32:03.869294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1103,7 +1103,7 @@
"search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
- "sort_field": "idx desc,modified desc",
+ "sort_field": "modified",
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index c9b8a37..decf522 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -724,7 +724,6 @@
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -738,7 +737,6 @@
repost_stock(new_name, warehouse)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
@frappe.whitelist()
def copy_specification_from_item_group(self):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 8b1224b..4028d93 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -534,8 +534,6 @@
def test_index_creation(self):
"check if index is getting created in db"
- from erpnext.stock.doctype.item.item import on_doctype_update
- on_doctype_update()
indices = frappe.db.sql("show index from tabItem", as_dict=1)
expected_columns = {"item_code", "item_name", "item_group", "route"}
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index d717c50..103e8d6 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -80,6 +80,9 @@
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
# Though the creation of Material Request from a Production Plan can be rethought to fix this
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
def set_title(self):
'''Set title as comma separated list of items'''
if not self.title:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 762f45f..c97b306 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -118,6 +118,10 @@
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
def validate_cwip_accounts(self):
for item in self.get('items'):
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 01cceb1..b2ad07f 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -168,8 +168,8 @@
for row in riv_entries:
doc = frappe.get_doc('Repost Item Valuation', row.name)
if doc.status in ('Queued', 'In Progress'):
- doc.deduplicate_similar_repost()
repost(doc)
+ doc.deduplicate_similar_repost()
riv_entries = get_repost_item_valuation_entries()
if riv_entries:
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index de79316..78b432d 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -4,12 +4,14 @@
import unittest
import frappe
+from frappe.utils import nowdate
from erpnext.controllers.stock_controller import create_item_wise_repost_entries
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot,
)
+from erpnext.stock.utils import PendingRepostingError
class TestRepostItemValuation(unittest.TestCase):
@@ -138,3 +140,25 @@
# to avoid breaking other tests accidentaly
riv4.set_status("Skipped")
riv3.set_status("Skipped")
+
+ def test_stock_freeze_validation(self):
+
+ today = nowdate()
+
+ riv = frappe.get_doc(
+ doctype="Repost Item Valuation",
+ item_code="_Test Item",
+ warehouse="_Test Warehouse - _TC",
+ based_on="Item and Warehouse",
+ posting_date=today,
+ posting_time="00:01:00",
+ )
+ riv.flags.dont_run_in_test = True # keep it queued
+ riv.submit()
+
+ stock_settings = frappe.get_doc("Stock Settings")
+ stock_settings.stock_frozen_upto = today
+
+ self.assertRaises(PendingRepostingError, stock_settings.save)
+
+ riv.set_status("Skipped")
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d31e65a..a00d63e 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -103,6 +103,8 @@
self.set_actual_qty()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
+ self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+ self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def on_submit(self):
self.update_stock_ledger()
@@ -545,7 +547,7 @@
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries
- if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
+ if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 0679467..5a9e77e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -24,7 +24,8 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args):
@@ -38,9 +39,10 @@
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
values, as_dict=1)
-class TestStockEntry(unittest.TestCase):
+class TestStockEntry(ERPNextTestCase):
def tearDown(self):
frappe.set_user("Administrator")
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -582,6 +584,65 @@
self.assertEqual(fg_cost,
flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
+ def test_work_order_manufacture_with_material_consumption(self):
+ from erpnext.manufacturing.doctype.work_order.work_order import (
+ make_stock_entry as _make_stock_entry,
+ )
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
+
+ bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
+ "is_default": 1, "docstatus": 1})
+
+ work_order = frappe.new_doc("Work Order")
+ work_order.update({
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test FG Item",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC"
+ })
+ work_order.insert()
+ work_order.submit()
+
+ make_stock_entry(item_code="_Test Item",
+ target="Stores - _TC", qty=10, basic_rate=5000.0)
+ make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="Stores - _TC", qty=10, basic_rate=1000.0)
+
+
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+ for d in s.get("items"):
+ d.s_warehouse = "Stores - _TC"
+ s.insert()
+ s.submit()
+
+ # When Stock Entry has RM and FG
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
+ s.save()
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
+ self.assertEqual(fg_cost,
+ flt(rm_cost - scrap_cost, 2))
+
+ # When Stock Entry has only FG + Scrap
+ s.items.pop(0)
+ s.items.pop(0)
+ s.submit()
+
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ self.assertEqual(rm_cost, 0)
+ expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
def test_variant_work_order(self):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
@@ -868,6 +929,83 @@
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs)
+ @change_settings("Stock Settings", {"allow_negative_stock": 0})
+ def test_future_negative_sle(self):
+ # Initialize item, batch, warehouse, opening qty
+ item_code = '_Test Future Neg Item'
+ batch_no = '_Test Future Neg Batch'
+ warehouses = [
+ '_Test Future Neg Warehouse Source',
+ '_Test Future Neg Warehouse Destination'
+ ]
+ warehouse_names = initialize_records_for_future_negative_sle_test(
+ item_code, batch_no, warehouses,
+ opening_qty=2, posting_date='2021-07-01'
+ )
+
+ # Executing an illegal sequence should raise an error
+ sequence_of_entries = [
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[0],
+ to_warehouse=warehouse_names[1],
+ batch_no=batch_no,
+ posting_date='2021-07-03',
+ purpose='Material Transfer'),
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[1],
+ to_warehouse=warehouse_names[0],
+ batch_no=batch_no,
+ posting_date='2021-07-04',
+ purpose='Material Transfer'),
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[0],
+ to_warehouse=warehouse_names[1],
+ batch_no=batch_no,
+ posting_date='2021-07-02', # Illegal SE
+ purpose='Material Transfer')
+ ]
+
+ self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
+
+ @change_settings("Stock Settings", {"allow_negative_stock": 0})
+ def test_future_negative_sle_batch(self):
+ from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+ # Initialize item, batch, warehouse, opening qty
+ item_code = '_Test MultiBatch Item'
+ TestBatch.make_batch_item(item_code)
+
+ batch_nos = [] # store generate batches
+ warehouse = '_Test Warehouse - _TC'
+
+ se1 = make_stock_entry(
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date='2021-09-01',
+ purpose='Material Receipt'
+ )
+ batch_nos.append(se1.items[0].batch_no)
+ se2 = make_stock_entry(
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date='2021-09-03',
+ purpose='Material Receipt'
+ )
+ batch_nos.append(se2.items[0].batch_no)
+
+ with self.assertRaises(NegativeStockError) as nse:
+ make_stock_entry(item_code=item_code,
+ qty=1,
+ from_warehouse=warehouse,
+ batch_no=batch_nos[1],
+ posting_date='2021-09-02', # backdated consumption of 2nd batch
+ purpose='Material Issue')
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
@@ -938,3 +1076,31 @@
]
test_records = frappe.get_test_records('Stock Entry')
+
+def initialize_records_for_future_negative_sle_test(
+ item_code, batch_no, warehouses, opening_qty, posting_date):
+ from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ TestBatch.make_batch_item(item_code)
+ make_new_batch(item_code=item_code, batch_id=batch_no)
+ warehouse_names = [create_warehouse(w) for w in warehouses]
+ create_stock_reconciliation(
+ purpose='Opening Stock',
+ posting_date=posting_date,
+ posting_time='20:00:20',
+ item_code=item_code,
+ warehouse=warehouse_names[0],
+ valuation_rate=100,
+ qty=opening_qty,
+ batch_no=batch_no,
+ )
+ return warehouse_names
+
+
+def create_stock_entries(sequence_of_entries):
+ for entry_detail in sequence_of_entries:
+ make_stock_entry(**entry_detail)
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 93bca7a..c538307 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
from frappe import _
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
-from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -43,7 +43,6 @@
def on_submit(self):
self.check_stock_frozen_date()
- self.actual_amt_check()
self.calculate_batch_qty()
if not self.get("via_landed_cost_voucher"):
@@ -57,18 +56,6 @@
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
- def actual_amt_check(self):
- """Validate that qty at warehouse for selected batch is >=0"""
- if self.batch_no and not self.get("allow_negative_stock"):
- batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
- from `tabStock Ledger Entry`
- where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
- (self.warehouse, self.item_code, self.batch_no))[0][0])
-
- if batch_bal_after_transaction < 0:
- frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
- .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
-
def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory:
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 1de48b6..c1293cb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -11,6 +11,8 @@
from frappe.utils import cint
from frappe.utils.html_utils import clean_html
+from erpnext.stock.utils import check_pending_reposting
+
class StockSettings(Document):
def validate(self):
@@ -36,6 +38,7 @@
self.validate_warehouses()
self.cant_change_valuation_method()
self.validate_clean_description_html()
+ self.validate_pending_reposts()
def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -64,6 +67,11 @@
# changed to text
frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
+ def validate_pending_reposts(self):
+ if self.stock_frozen_upto:
+ check_pending_reposting(self.stock_frozen_upto)
+
+
def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer()
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index ca92936..26db264 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -33,65 +33,6 @@
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0)
- def test_warehouse_renaming(self):
- create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
- account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
- self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
-
- # Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
-
- def test_warehouse_merging(self):
- company = "_Test Company with perpetual inventory"
- create_warehouse("Test Warehouse for Merging 1", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 2", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
- qty=1, rate=100, company=company)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
- qty=1, rate=100, company=company)
-
- existing_bin_qty = (
- cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
- + cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
- )
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
- "Test Warehouse for Merging 2 - TCP1", merge=True)
-
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
-
- bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
-
- self.assertEqual(bin_qty, existing_bin_qty)
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
-
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 9b90932..05076b5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_import": 1,
- "allow_rename": 1,
"creation": "2013-03-07 18:50:32",
"description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType",
@@ -245,7 +244,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-04-09 19:54:56.263965",
+ "modified": "2021-12-03 04:40:06.414630",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index b9dbc38..9cfad86 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -10,7 +10,6 @@
from frappe.utils import cint, flt
from frappe.utils.nestedset import NestedSet
-import erpnext
from erpnext.stock import get_warehouse_account
@@ -68,57 +67,6 @@
return frappe.db.sql("""select name from `tabWarehouse`
where parent_warehouse = %s limit 1""", self.name)
- def before_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).before_rename(old_name, new_name, merge)
-
- # Add company abbr if not provided
- new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
-
- if merge:
- if not frappe.db.exists("Warehouse", new_warehouse):
- frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
-
- if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
- frappe.throw(_("Both Warehouse must belong to same Company"))
-
- return new_warehouse
-
- def after_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).after_rename(old_name, new_name, merge)
-
- new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
- self.db_set("warehouse_name", new_warehouse_name)
-
- if merge:
- self.recalculate_bin_qty(new_name)
-
- def get_new_warehouse_name_without_abbr(self, name):
- company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
- parts = name.rsplit(" - ", 1)
-
- if parts[-1].lower() == company_abbr.lower():
- name = parts[0]
-
- return name
-
- def recalculate_bin_qty(self, new_name):
- from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
- existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
-
- repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
- from tabBin where warehouse=%s""", new_name)
-
- # Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
-
- for item_code in repost_stock_for_items:
- repost_stock(item_code, new_name)
-
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
-
def convert_to_group_or_ledger(self):
if self.is_group:
self.convert_to_ledger()
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9d40982..e95c0fc 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -7,9 +7,10 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
import erpnext
+from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
@@ -17,19 +18,15 @@
)
-# future reposting
class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
_exceptions = frappe.local('stockledger_exceptions')
-# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
- from erpnext.stock.utils import update_bin
-
cancel = sl_entries[0].get("is_cancelled")
if cancel:
validate_cancellation(sl_entries)
@@ -64,7 +61,38 @@
# preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
- update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+ is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
+ if is_stock_item:
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_bin_qty(bin_name, args)
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
+ else:
+ frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
+
+def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+ if not args.get("posting_date"):
+ args["posting_date"] = nowdate()
+
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
+ update_entries_after({
+ "item_code": args.get('item_code'),
+ "warehouse": args.get('warehouse'),
+ "posting_date": args.get("posting_date"),
+ "posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
+ "voucher_no": args.get("voucher_no"),
+ "sle_id": args.get('name'),
+ "creation": args.get('creation')
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ # update qty in future sle and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def get_args_for_future_sle(row):
return frappe._dict({
@@ -803,9 +831,9 @@
def update_bin(self):
# update bin for each warehouse
for warehouse, data in self.data.items():
- bin_record = get_or_make_bin(self.item_code, warehouse)
+ bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_record, {
+ frappe.db.set_value('Bin', bin_name, {
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
@@ -1061,17 +1089,36 @@
allow_negative_stock = cint(allow_negative_stock) \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
- sle = get_future_sle_with_negative_qty(args)
- if sle:
- message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(sle[0]["qty_after_transaction"]),
- frappe.get_desk_link('Item', args.item_code),
- frappe.get_desk_link('Warehouse', args.warehouse),
- sle[0]["posting_date"], sle[0]["posting_time"],
- frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+ if allow_negative_stock:
+ return
+ if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
+ return
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ neg_sle = get_future_sle_with_negative_qty(args)
+ if neg_sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(neg_sle[0]["qty_after_transaction"]),
+ frappe.get_desk_link('Item', args.item_code),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
+
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+
+ if not args.batch_no:
+ return
+
+ neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
+ if neg_batch_sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(neg_batch_sle[0]["cumulative_total"]),
+ frappe.get_desk_link('Batch', args.batch_no),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
+ frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+
def get_future_sle_with_negative_qty(args):
return frappe.db.sql("""
@@ -1090,6 +1137,29 @@
limit 1
""", args, as_dict=1)
+
+def get_future_sle_with_negative_batch_qty(args):
+ return frappe.db.sql("""
+ with batch_ledger as (
+ select
+ posting_date, posting_time, voucher_type, voucher_no,
+ sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and batch_no=%(batch_no)s
+ and is_cancelled = 0
+ order by posting_date, posting_time, creation
+ )
+ select * from batch_ledger
+ where
+ cumulative_total < 0.0
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ limit 1
+ """, args, as_dict=1)
+
+
def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
""" Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 6.
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8031c58..3b1ae3b 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,6 +12,7 @@
class InvalidWarehouseCompany(frappe.ValidationError): pass
+class PendingRepostingError(frappe.ValidationError): pass
def get_stock_value_from_bin(warehouse=None, item_code=None):
values = {}
@@ -187,7 +188,7 @@
bin_obj.flags.ignore_permissions = True
return bin_obj
-def get_or_make_bin(item_code, warehouse) -> str:
+def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
@@ -203,11 +204,12 @@
return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
- bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
- update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
@@ -416,3 +418,28 @@
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
if reposting_in_progress:
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
+
+def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
+ """Check if there are pending reposting job till the specified posting date."""
+
+ filters = {
+ "docstatus": 1,
+ "status": ["in", ["Queued","In Progress", "Failed"]],
+ "posting_date": ["<=", posting_date],
+ }
+
+ reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
+ if reposting_pending and throw_error:
+ msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.")
+ frappe.msgprint(msg,
+ raise_exception=PendingRepostingError,
+ title="Stock Reposting Ongoing",
+ indicator="red",
+ primary_action={
+ "label": _("Show pending entries"),
+ "client_action": "erpnext.route_to_pending_reposts",
+ "args": filters,
+ }
+ )
+
+ return bool(reposting_pending)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index ca03a78..d46ffb5 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1847,7 +1847,7 @@
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
Owner,Besitzer,
-PAN,PFANNE,
+PAN,PAN,
POS,Verkaufsstelle,
POS Profile,Verkaufsstellen-Profil,
POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden",
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 14b3afa..1d8b3a8 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -162,6 +162,28 @@
return ret
+ def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str):
+ """ Reset "Set default X" fields on forms to avoid confusion.
+
+ example:
+ doc = {
+ "set_from_warehouse": "Warehouse A",
+ "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}],
+ }
+ Since this has dissimilar values in child table, the default field will be erased.
+
+ doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ """
+ child_table_values = set()
+
+ for row in self.get(child_table):
+ child_table_values.add(row.get(child_table_field))
+
+ if len(child_table_values) > 1:
+ self.set(default_field, None)
+ else:
+ self.set(default_field, list(child_table_values)[0])
+
def delete_events(ref_type, ref_name):
events = frappe.db.sql_list(""" SELECT
distinct `tabEvent`.name