Merge branch 'develop' into exit-interview
This commit is contained in:
commit
1867bad693
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
106
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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
|
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,7 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea to improve ERPNext
|
||||
title: ''
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
@ -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
56
.github/stale.yml
vendored
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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": [
|
||||
{
|
||||
|
22
erpnext/controllers/tests/test_transaction_base.py
Normal file
22
erpnext/controllers/tests/test_transaction_base.py
Normal 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)
|
||||
|
@ -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",
|
||||
|
@ -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() });
|
||||
}
|
||||
},
|
||||
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal 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)
|
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal 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')
|
@ -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();
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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:
|
||||
|
@ -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'):
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user