diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c145291b57..0000000000 --- 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 ---- - - - -## 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 0000000000..a6e16a03d8 --- /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 6cdad356cd..418bf3c941 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: '' + --- \n \n Invoice#: {{doc.name}}\n \u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}\n \n \n Invoice Date: {{doc.posting_date}}\n \u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}\n \n \n Date of Supply:{{doc.posting_date}}\n \u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}\n \n \n \n \n Supplier:\n \u0627\u0644\u0645\u0648\u0631\u062f:\n \n\t\t{% if (company.tax_id) %}\n \n Supplier Tax Identification Number:\n \u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:\n \n \n {{ company.tax_id }}\n {{ company.tax_id }}\n \n {% endif %}\n \n {{ company.name }}\n {{ company.company_name_in_arabic }} \n \n \n \n {% if(supplier_address_doc) %}\n \n {{ supplier_address_doc.address_line1}} \n {{ supplier_address_doc.address_in_arabic}} \n \n \n Phone: {{ supplier_address_doc.phone }}\n \u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}\n \n \n Email: {{ supplier_address_doc.email_id }}\n \u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}\n \n {% endif %}\n \n \n \n CUSTOMER:\n \u0639\u0645\u064a\u0644:\n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n Customer Tax Identification Number:\n \u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:\n \n \n {{ customer_tax_id }}\n {{ customer_tax_id }}\n \n {% endif %}\n \n {{ doc.customer }}\n {{ doc.customer_name_in_arabic }} \n \n \n {% if(customer_address) %}\n \n {{ customer_address.address_line1}} \n {{ customer_address.address_in_arabic}} \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n SHIPPING ADDRESS:\n \u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:\n \n \n \n {{ customer_shipping_address.address_line1}} \n {{ customer_shipping_address.address_in_arabic}} \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n OTHER INFORMATION\n \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649\n \n \n \n Purchase Order Number: {{ doc.po_no }}\n \u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}\n \n {% endif %}\n \n \n Payment Due Date: {{ doc.due_date}} \n \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}\n \n \n \n\n \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 \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \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 \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\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
\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
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\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 \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \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 \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \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 \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\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
\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
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\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 38a089c632..2e31c03d5c 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 @@ def setup(company=None, patch=True): 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 @@ def make_custom_fields(): 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 7d00d8b392..a03c3f0994 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -4,144 +4,146 @@ from base64 import b64encode 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.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])) + # 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) - name = frappe.generate_hash(doc.name, 5) + name = frappe.generate_hash(doc.name, 5) - # 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": "qr_code" - }) + # 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" + }) - _file.save() + _file.save() - # assigning to document - doc.db_set('qr_code', _file.file_url) - doc.notify_update() - - 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 1c2482568f..cc951850a4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -63,6 +63,8 @@ class SalesOrder(SellingController): 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 @@ def make_work_orders(items, sales_order, company, project=None): 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/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 52684607b4..70d48a42d7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ class DeliveryNote(SellingController): 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/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index d717c50919..103e8d6a88 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -80,6 +80,9 @@ class MaterialRequest(BuyingController): # 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 762f45f75f..c97b306c4e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -118,6 +118,10 @@ class PurchaseReceipt(BuyingController): 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 01cceb176b..b2ad07f9c3 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 @@ def repost_entries(): 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 de793163fd..78b432d564 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 @@ class TestRepostItemValuation(unittest.TestCase): # 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 a38dfa5062..a00d63e6f2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -103,6 +103,8 @@ class StockEntry(StockController): 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() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5ef07705d8..5a9e77e325 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.stock_reconciliation import ( 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,7 +39,7 @@ def get_sle(**args): 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") @@ -928,6 +929,83 @@ class TestStockEntry(unittest.TestCase): 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]) @@ -998,3 +1076,31 @@ def get_multiple_items(): ] 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 93bca7a694..c53830799d 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 @@ import frappe 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 @@ class StockLedgerEntry(Document): 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 @@ class StockLedgerEntry(Document): "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 1de48b6f1f..c1293cbf0f 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.model.document import Document 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 @@ class StockSettings(Document): 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 @@ class StockSettings(Document): # 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/stock_ledger.py b/erpnext/stock/stock_ledger.py index d78632a0f3..e95c0fcd23 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1089,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): 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 + + 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") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1118,6 +1137,29 @@ def get_future_sle_with_negative_qty(args): 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 72d8098d44..3b1ae3b43c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,6 +12,7 @@ import erpnext class InvalidWarehouseCompany(frappe.ValidationError): pass +class PendingRepostingError(frappe.ValidationError): pass def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -417,3 +418,28 @@ def is_reposting_item_valuation_in_progress(): {'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 ca03a787cd..d46ffb5609 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1847,7 +1847,7 @@ Overdue,Überfällig, 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 14b3afa5a7..1d8b3a8db6 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -162,6 +162,28 @@ class TransactionBase(StatusUpdater): 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