Merge branch 'develop' into exit-interview

This commit is contained in:
Rucha Mahabal 2021-12-12 22:13:11 +05:30 committed by GitHub
commit 1867bad693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 651 additions and 229 deletions

View File

@ -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.

106
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -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

View File

@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request
assignees: ''
---
<!--

View File

@ -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.**

56
.github/stale.yml vendored
View File

@ -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
# 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
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking as stale
staleLabel: inactive
# 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
limitPerRun: 10
# Limit to only `issues` or `pulls`
only: pulls
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
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.
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.

View File

@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
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 @@ class AccountsSettings(Document):
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 @@ class AccountsSettings(Document):
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)

View File

@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController):
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):

View File

@ -155,6 +155,8 @@ class SalesInvoice(SellingController):
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:

View File

@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date":
entry_date = row.due_date
# use posting date as a fallback for advances posted via journal and payment entry
# when ageing viewed by due date
entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date
else:

View File

@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted:
if tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name')
@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers:
supplier_map[d.name] = d

View File

@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
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({

View File

@ -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": [
{

View File

@ -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)

View File

@ -265,6 +265,9 @@ doc_events = {
"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",

View File

@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', {
}
});
}
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() });
}
},

View File

@ -4,10 +4,17 @@ from frappe import _
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']
}
]
}

View File

@ -314,4 +314,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.v14_0.add_default_exit_questionnaire_notification_template
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template

View File

@ -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)

View File

@ -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')

View File

@ -84,6 +84,10 @@ $.extend(erpnext, {
});
},
route_to_pending_reposts: (args) => {
frappe.set_route('List', 'Repost Item Valuation', args);
},
proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog();

View File

@ -213,10 +213,11 @@ def get_regional_address_details(party_details, doctype, company):
tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
if tax_template_by_category:
party_details.get['taxes_and_charges'] = 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
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",

View File

@ -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"
}

File diff suppressed because one or more lines are too long

View File

@ -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
)

View File

@ -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()
if frappe.db.exists('KSA VAT Setting', doc.name):
frappe.delete_doc('KSA VAT Setting', doc.name)

View File

@ -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)

View File

@ -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({

View File

@ -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:

View File

@ -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'):

View File

@ -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:

View File

@ -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")

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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()

View File

@ -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.

View File

@ -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)

View File

@ -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",

Can't render this file because it is too large.

View File

@ -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