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
|
name: Feature request
|
||||||
about: Suggest an idea to improve ERPNext
|
about: Suggest an idea to improve ERPNext
|
||||||
|
title: ''
|
||||||
labels: feature-request
|
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
|
# 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
|
# Label to use when marking as stale
|
||||||
staleLabel: inactive
|
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
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
limitPerRun: 30
|
limitPerRun: 10
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
only: pulls
|
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.model.document import Document
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
from erpnext.stock.utils import check_pending_reposting
|
||||||
|
|
||||||
|
|
||||||
class AccountsSettings(Document):
|
class AccountsSettings(Document):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
@ -25,6 +27,7 @@ class AccountsSettings(Document):
|
|||||||
self.validate_stale_days()
|
self.validate_stale_days()
|
||||||
self.enable_payment_schedule_in_print()
|
self.enable_payment_schedule_in_print()
|
||||||
self.toggle_discount_accounting_fields()
|
self.toggle_discount_accounting_fields()
|
||||||
|
self.validate_pending_reposts()
|
||||||
|
|
||||||
def validate_stale_days(self):
|
def validate_stale_days(self):
|
||||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
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(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)
|
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.set_status()
|
||||||
self.validate_purchase_receipt_if_update_stock()
|
self.validate_purchase_receipt_if_update_stock()
|
||||||
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
|
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):
|
def validate_release_date(self):
|
||||||
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
|
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:
|
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)
|
validate_loyalty_points(self, self.loyalty_points)
|
||||||
|
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_fixed_asset(self):
|
def validate_fixed_asset(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
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):
|
def set_ageing(self, row):
|
||||||
if self.filters.ageing_based_on == "Due Date":
|
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":
|
elif self.filters.ageing_based_on == "Supplier Invoice Date":
|
||||||
entry_date = row.bill_date
|
entry_date = row.bill_date
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
|||||||
posting_date = entry.posting_date
|
posting_date = entry.posting_date
|
||||||
voucher_type = entry.voucher_type
|
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:
|
if entry.account in tds_accounts:
|
||||||
tds_deducted += (entry.credit - entry.debit)
|
tds_deducted += (entry.credit - entry.debit)
|
||||||
|
|
||||||
total_amount_credited += (entry.credit - entry.debit)
|
total_amount_credited += (entry.credit - entry.debit)
|
||||||
|
|
||||||
if rate and tds_deducted:
|
if tds_deducted:
|
||||||
row = {
|
row = {
|
||||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
'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():
|
def get_supplier_pan_map():
|
||||||
supplier_map = frappe._dict()
|
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:
|
for d in suppliers:
|
||||||
supplier_map[d.name] = d
|
supplier_map[d.name] = d
|
||||||
|
|||||||
@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
self.create_raw_materials_supplied("supplied_items")
|
self.create_raw_materials_supplied("supplied_items")
|
||||||
self.set_received_qty_for_drop_ship_items()
|
self.set_received_qty_for_drop_ship_items()
|
||||||
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
|
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):
|
def validate_with_previous_doc(self):
|
||||||
super(PurchaseOrder, self).validate_with_previous_doc({
|
super(PurchaseOrder, self).validate_with_previous_doc({
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"company",
|
"company",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"valid_till",
|
"valid_till",
|
||||||
|
"quotation_number",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"address_section",
|
"address_section",
|
||||||
"supplier_address",
|
"supplier_address",
|
||||||
@ -797,6 +798,11 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Valid Till"
|
"label": "Valid Till"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quotation_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Quotation Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
@ -804,10 +810,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 00:58:20.995491",
|
"modified": "2021-12-11 06:43:20.924080",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation",
|
"name": "Supplier Quotation",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"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"
|
"erpnext.regional.india.utils.update_taxable_values"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"POS Invoice": {
|
||||||
|
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
|
||||||
|
},
|
||||||
"Purchase Invoice": {
|
"Purchase Invoice": {
|
||||||
"validate": [
|
"validate": [
|
||||||
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
|
"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) {
|
if (!frm.doc.status) {
|
||||||
frm.set_value({ status: 'Draft' });
|
frm.set_value({ status: 'Draft' });
|
||||||
}
|
}
|
||||||
if (frm.doc.__islocal) {
|
if (frm.doc.__islocal) {
|
||||||
|
frm.clear_table("purposes");
|
||||||
frm.set_value({ mntc_date: frappe.datetime.get_today() });
|
frm.set_value({ mntc_date: frappe.datetime.get_today() });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,10 +4,17 @@ from frappe import _
|
|||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
'fieldname': 'job_card',
|
'fieldname': 'job_card',
|
||||||
|
'non_standard_fieldnames': {
|
||||||
|
'Quality Inspection': 'reference_name'
|
||||||
|
},
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
'label': _('Transactions'),
|
'label': _('Transactions'),
|
||||||
'items': ['Material Request', 'Stock Entry']
|
'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.v14_0.delete_hub_doctypes
|
||||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||||
erpnext.patches.v14_0.migrate_crm_settings
|
erpnext.patches.v14_0.migrate_crm_settings
|
||||||
|
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||||
|
erpnext.patches.v13_0.disable_ksa_print_format_for_others
|
||||||
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
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: () => {
|
proceed_save_with_reminders_frequency_change: () => {
|
||||||
frappe.ui.hide_open_dialog();
|
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)
|
tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||||
|
|
||||||
if tax_template_by_category:
|
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
|
return
|
||||||
|
|
||||||
if not party_details.place_of_supply: return party_details
|
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
|
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",
|
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
|
import frappe
|
||||||
from frappe.permissions import add_permission, update_permission_property
|
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 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
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
@ -13,6 +13,16 @@ def setup(company=None, patch=True):
|
|||||||
add_permissions()
|
add_permissions()
|
||||||
make_custom_fields()
|
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():
|
def add_permissions():
|
||||||
"""Add Permissions for KSA VAT Setting."""
|
"""Add Permissions for KSA VAT Setting."""
|
||||||
add_permission('KSA VAT Setting', 'All', 0)
|
add_permission('KSA VAT Setting', 'All', 0)
|
||||||
@ -33,8 +43,16 @@ def make_custom_fields():
|
|||||||
custom_fields = {
|
custom_fields = {
|
||||||
'Sales Invoice': [
|
'Sales Invoice': [
|
||||||
dict(
|
dict(
|
||||||
fieldname='qr_code',
|
fieldname='ksa_einv_qr',
|
||||||
label='QR Code',
|
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',
|
fieldtype='Attach Image',
|
||||||
read_only=1, no_copy=1, hidden=1
|
read_only=1, no_copy=1, hidden=1
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,144 +4,146 @@ from base64 import b64encode
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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 frappe.utils.data import add_to_date, get_time, getdate
|
||||||
from pyqrcode import create as qr_create
|
from pyqrcode import create as qr_create
|
||||||
|
|
||||||
from erpnext import get_region
|
from erpnext import get_region
|
||||||
|
|
||||||
|
|
||||||
def create_qr_code(doc, method):
|
def create_qr_code(doc, method=None):
|
||||||
"""Create QR Code after inserting Sales Inv
|
|
||||||
"""
|
|
||||||
|
|
||||||
region = get_region(doc.company)
|
region = get_region(doc.company)
|
||||||
if region not in ['Saudi Arabia']:
|
if region not in ['Saudi Arabia']:
|
||||||
return
|
return
|
||||||
|
|
||||||
# if QR Code field not present, do nothing
|
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
|
||||||
if not hasattr(doc, 'qr_code'):
|
if not hasattr(doc, 'ksa_einv_qr'):
|
||||||
return
|
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
|
# 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}):
|
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
|
||||||
return
|
return
|
||||||
|
|
||||||
meta = frappe.get_meta('Sales Invoice')
|
meta = frappe.get_meta(doc.doctype)
|
||||||
|
|
||||||
for field in meta.get_image_fields():
|
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
|
||||||
if field.fieldname == 'qr_code':
|
''' TLV conversion for
|
||||||
''' TLV conversion for
|
1. Seller's Name
|
||||||
1. Seller's Name
|
2. VAT Number
|
||||||
2. VAT Number
|
3. Time Stamp
|
||||||
3. Time Stamp
|
4. Invoice Amount
|
||||||
4. Invoice Amount
|
5. VAT Amount
|
||||||
5. VAT Amount
|
'''
|
||||||
'''
|
tlv_array = []
|
||||||
tlv_array = []
|
# Sellers Name
|
||||||
# Sellers Name
|
|
||||||
|
|
||||||
seller_name = frappe.db.get_value(
|
seller_name = frappe.db.get_value(
|
||||||
'Company',
|
'Company',
|
||||||
doc.company,
|
doc.company,
|
||||||
'company_name_in_arabic')
|
'company_name_in_arabic')
|
||||||
|
|
||||||
if not seller_name:
|
if not seller_name:
|
||||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||||
|
|
||||||
tag = bytes([1]).hex()
|
tag = bytes([1]).hex()
|
||||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||||
value = seller_name.encode('utf-8').hex()
|
value = seller_name.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# VAT Number
|
# VAT Number
|
||||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||||
if not tax_id:
|
if not tax_id:
|
||||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||||
|
|
||||||
tag = bytes([2]).hex()
|
tag = bytes([2]).hex()
|
||||||
length = bytes([len(tax_id)]).hex()
|
length = bytes([len(tax_id)]).hex()
|
||||||
value = tax_id.encode('utf-8').hex()
|
value = tax_id.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Time Stamp
|
# Time Stamp
|
||||||
posting_date = getdate(doc.posting_date)
|
posting_date = getdate(doc.posting_date)
|
||||||
time = get_time(doc.posting_time)
|
time = get_time(doc.posting_time)
|
||||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
tag = bytes([3]).hex()
|
tag = bytes([3]).hex()
|
||||||
length = bytes([len(time_stamp)]).hex()
|
length = bytes([len(time_stamp)]).hex()
|
||||||
value = time_stamp.encode('utf-8').hex()
|
value = time_stamp.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Invoice Amount
|
# Invoice Amount
|
||||||
invoice_amount = str(doc.grand_total)
|
invoice_amount = str(doc.grand_total)
|
||||||
tag = bytes([4]).hex()
|
tag = bytes([4]).hex()
|
||||||
length = bytes([len(invoice_amount)]).hex()
|
length = bytes([len(invoice_amount)]).hex()
|
||||||
value = invoice_amount.encode('utf-8').hex()
|
value = invoice_amount.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# VAT Amount
|
# VAT Amount
|
||||||
vat_amount = str(doc.total_taxes_and_charges)
|
vat_amount = str(doc.total_taxes_and_charges)
|
||||||
|
|
||||||
tag = bytes([5]).hex()
|
tag = bytes([5]).hex()
|
||||||
length = bytes([len(vat_amount)]).hex()
|
length = bytes([len(vat_amount)]).hex()
|
||||||
value = vat_amount.encode('utf-8').hex()
|
value = vat_amount.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Joining bytes into one
|
# Joining bytes into one
|
||||||
tlv_buff = ''.join(tlv_array)
|
tlv_buff = ''.join(tlv_array)
|
||||||
|
|
||||||
# base64 conversion for QR Code
|
# base64 conversion for QR Code
|
||||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||||
|
|
||||||
qr_image = io.BytesIO()
|
qr_image = io.BytesIO()
|
||||||
url = qr_create(base64_string, error='L')
|
url = qr_create(base64_string, error='L')
|
||||||
url.png(qr_image, scale=2, quiet_zone=1)
|
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
|
# making file
|
||||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||||
_file = frappe.get_doc({
|
_file = frappe.get_doc({
|
||||||
"doctype": "File",
|
"doctype": "File",
|
||||||
"file_name": filename,
|
"file_name": filename,
|
||||||
"is_private": 0,
|
"is_private": 0,
|
||||||
"content": qr_image.getvalue(),
|
"content": qr_image.getvalue(),
|
||||||
"attached_to_doctype": doc.get("doctype"),
|
"attached_to_doctype": doc.get("doctype"),
|
||||||
"attached_to_name": doc.get("name"),
|
"attached_to_name": doc.get("name"),
|
||||||
"attached_to_field": "qr_code"
|
"attached_to_field": "ksa_einv_qr"
|
||||||
})
|
})
|
||||||
|
|
||||||
_file.save()
|
_file.save()
|
||||||
|
|
||||||
# assigning to document
|
# assigning to document
|
||||||
doc.db_set('qr_code', _file.file_url)
|
doc.db_set('ksa_einv_qr', _file.file_url)
|
||||||
doc.notify_update()
|
doc.notify_update()
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def delete_qr_code_file(doc, method):
|
def delete_qr_code_file(doc, method=None):
|
||||||
"""Delete QR Code on deleted sales invoice"""
|
|
||||||
|
|
||||||
region = get_region(doc.company)
|
region = get_region(doc.company)
|
||||||
if region not in ['Saudi Arabia']:
|
if region not in ['Saudi Arabia']:
|
||||||
return
|
return
|
||||||
|
|
||||||
if hasattr(doc, 'qr_code'):
|
if hasattr(doc, 'ksa_einv_qr'):
|
||||||
if doc.get('qr_code'):
|
if doc.get('ksa_einv_qr'):
|
||||||
file_doc = frappe.get_list('File', {
|
file_doc = frappe.get_list('File', {
|
||||||
'file_url': doc.get('qr_code')
|
'file_url': doc.get('ksa_einv_qr')
|
||||||
})
|
})
|
||||||
if len(file_doc):
|
if len(file_doc):
|
||||||
frappe.delete_doc('File', file_doc[0].name)
|
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':
|
if doc.country != 'Saudi Arabia':
|
||||||
return
|
return
|
||||||
|
|
||||||
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
|
if frappe.db.exists('KSA VAT Setting', doc.name):
|
||||||
settings_doc.delete()
|
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.billing_status: self.billing_status = 'Not Billed'
|
||||||
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
||||||
|
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_po(self):
|
def validate_po(self):
|
||||||
# validate p.o date v/s delivery date
|
# validate p.o date v/s delivery date
|
||||||
if self.po_date and not self.skip_delivery_note:
|
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']
|
description=i['description']
|
||||||
)).insert()
|
)).insert()
|
||||||
work_order.set_work_order_operations()
|
work_order.set_work_order_operations()
|
||||||
|
work_order.flags.ignore_mandatory = True
|
||||||
work_order.save()
|
work_order.save()
|
||||||
out.append(work_order)
|
out.append(work_order)
|
||||||
|
|
||||||
|
|||||||
@ -138,6 +138,7 @@ class DeliveryNote(SellingController):
|
|||||||
self.update_current_stock()
|
self.update_current_stock()
|
||||||
|
|
||||||
if not self.installation_status: self.installation_status = 'Not Installed'
|
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):
|
def validate_with_previous_doc(self):
|
||||||
super(DeliveryNote, self).validate_with_previous_doc({
|
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
|
# 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
|
# 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):
|
def set_title(self):
|
||||||
'''Set title as comma separated list of items'''
|
'''Set title as comma separated list of items'''
|
||||||
if not self.title:
|
if not self.title:
|
||||||
|
|||||||
@ -118,6 +118,10 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if getdate(self.posting_date) > getdate(nowdate()):
|
if getdate(self.posting_date) > getdate(nowdate()):
|
||||||
throw(_("Posting Date cannot be future date"))
|
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):
|
def validate_cwip_accounts(self):
|
||||||
for item in self.get('items'):
|
for item in self.get('items'):
|
||||||
|
|||||||
@ -168,8 +168,8 @@ def repost_entries():
|
|||||||
for row in riv_entries:
|
for row in riv_entries:
|
||||||
doc = frappe.get_doc('Repost Item Valuation', row.name)
|
doc = frappe.get_doc('Repost Item Valuation', row.name)
|
||||||
if doc.status in ('Queued', 'In Progress'):
|
if doc.status in ('Queued', 'In Progress'):
|
||||||
doc.deduplicate_similar_repost()
|
|
||||||
repost(doc)
|
repost(doc)
|
||||||
|
doc.deduplicate_similar_repost()
|
||||||
|
|
||||||
riv_entries = get_repost_item_valuation_entries()
|
riv_entries = get_repost_item_valuation_entries()
|
||||||
if riv_entries:
|
if riv_entries:
|
||||||
|
|||||||
@ -4,12 +4,14 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import create_item_wise_repost_entries
|
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.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
|
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
|
||||||
in_configured_timeslot,
|
in_configured_timeslot,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.utils import PendingRepostingError
|
||||||
|
|
||||||
|
|
||||||
class TestRepostItemValuation(unittest.TestCase):
|
class TestRepostItemValuation(unittest.TestCase):
|
||||||
@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase):
|
|||||||
# to avoid breaking other tests accidentaly
|
# to avoid breaking other tests accidentaly
|
||||||
riv4.set_status("Skipped")
|
riv4.set_status("Skipped")
|
||||||
riv3.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.set_actual_qty()
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
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):
|
def on_submit(self):
|
||||||
self.update_stock_ledger()
|
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 (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
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):
|
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,
|
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
|
||||||
values, as_dict=1)
|
values, as_dict=1)
|
||||||
|
|
||||||
class TestStockEntry(unittest.TestCase):
|
class TestStockEntry(ERPNextTestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
|
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]
|
distributed_costs = [d.additional_cost for d in se.items]
|
||||||
self.assertEqual([40.0, 60.0], distributed_costs)
|
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):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
se = frappe.copy_doc(test_records[0])
|
se = frappe.copy_doc(test_records[0])
|
||||||
@ -998,3 +1076,31 @@ def get_multiple_items():
|
|||||||
]
|
]
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Stock Entry')
|
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 import _
|
||||||
from frappe.core.doctype.role.role import get_users
|
from frappe.core.doctype.role.role import get_users
|
||||||
from frappe.model.document import Document
|
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.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
||||||
@ -43,7 +43,6 @@ class StockLedgerEntry(Document):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.check_stock_frozen_date()
|
self.check_stock_frozen_date()
|
||||||
self.actual_amt_check()
|
|
||||||
self.calculate_batch_qty()
|
self.calculate_batch_qty()
|
||||||
|
|
||||||
if not self.get("via_landed_cost_voucher"):
|
if not self.get("via_landed_cost_voucher"):
|
||||||
@ -57,18 +56,6 @@ class StockLedgerEntry(Document):
|
|||||||
"sum(actual_qty)") or 0
|
"sum(actual_qty)") or 0
|
||||||
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
|
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):
|
def validate_mandatory(self):
|
||||||
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
|
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
|
||||||
for k in mandatory:
|
for k in mandatory:
|
||||||
|
|||||||
@ -11,6 +11,8 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
from frappe.utils.html_utils import clean_html
|
from frappe.utils.html_utils import clean_html
|
||||||
|
|
||||||
|
from erpnext.stock.utils import check_pending_reposting
|
||||||
|
|
||||||
|
|
||||||
class StockSettings(Document):
|
class StockSettings(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -36,6 +38,7 @@ class StockSettings(Document):
|
|||||||
self.validate_warehouses()
|
self.validate_warehouses()
|
||||||
self.cant_change_valuation_method()
|
self.cant_change_valuation_method()
|
||||||
self.validate_clean_description_html()
|
self.validate_clean_description_html()
|
||||||
|
self.validate_pending_reposts()
|
||||||
|
|
||||||
def validate_warehouses(self):
|
def validate_warehouses(self):
|
||||||
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
||||||
@ -64,6 +67,11 @@ class StockSettings(Document):
|
|||||||
# changed to text
|
# changed to text
|
||||||
frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
|
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):
|
def on_update(self):
|
||||||
self.toggle_warehouse_field_for_inter_warehouse_transfer()
|
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) \
|
allow_negative_stock = cint(allow_negative_stock) \
|
||||||
or cint(frappe.db.get_single_value("Stock Settings", "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:
|
if allow_negative_stock:
|
||||||
sle = get_future_sle_with_negative_qty(args)
|
return
|
||||||
if sle:
|
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
|
||||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
return
|
||||||
abs(sle[0]["qty_after_transaction"]),
|
|
||||||
frappe.get_desk_link('Item', args.item_code),
|
neg_sle = get_future_sle_with_negative_qty(args)
|
||||||
frappe.get_desk_link('Warehouse', args.warehouse),
|
if neg_sle:
|
||||||
sle[0]["posting_date"], sle[0]["posting_time"],
|
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||||
frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
|
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):
|
def get_future_sle_with_negative_qty(args):
|
||||||
return frappe.db.sql("""
|
return frappe.db.sql("""
|
||||||
@ -1118,6 +1137,29 @@ def get_future_sle_with_negative_qty(args):
|
|||||||
limit 1
|
limit 1
|
||||||
""", args, as_dict=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:
|
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
|
""" Rounds off the number to zero only if number is close to zero for decimal
|
||||||
specified in precision. Precision defaults to 6.
|
specified in precision. Precision defaults to 6.
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import erpnext
|
|||||||
|
|
||||||
|
|
||||||
class InvalidWarehouseCompany(frappe.ValidationError): pass
|
class InvalidWarehouseCompany(frappe.ValidationError): pass
|
||||||
|
class PendingRepostingError(frappe.ValidationError): pass
|
||||||
|
|
||||||
def get_stock_value_from_bin(warehouse=None, item_code=None):
|
def get_stock_value_from_bin(warehouse=None, item_code=None):
|
||||||
values = {}
|
values = {}
|
||||||
@ -417,3 +418,28 @@ def is_reposting_item_valuation_in_progress():
|
|||||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||||
if reposting_in_progress:
|
if reposting_in_progress:
|
||||||
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
|
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},
|
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
|
||||||
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
|
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
|
||||||
Owner,Besitzer,
|
Owner,Besitzer,
|
||||||
PAN,PFANNE,
|
PAN,PAN,
|
||||||
POS,Verkaufsstelle,
|
POS,Verkaufsstelle,
|
||||||
POS Profile,Verkaufsstellen-Profil,
|
POS Profile,Verkaufsstellen-Profil,
|
||||||
POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden",
|
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
|
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):
|
def delete_events(ref_type, ref_name):
|
||||||
events = frappe.db.sql_list(""" SELECT
|
events = frappe.db.sql_list(""" SELECT
|
||||||
distinct `tabEvent`.name
|
distinct `tabEvent`.name
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user